训练语料

word2vec的算法是公开的,word2vec模型的质量完全取决于训练语料的质量。目前免费开放的预料不多,中文语料更是凤毛麟角。

使用搜狗新闻预料生成word2vec-图1.png

这里推荐使用搜狗实验室的中文语料,对应的网址为:

http://www.sogou.com/labs/resource/cs.php

通常使用”搜狐新闻数据”即可,该数据来自搜狐新闻2012年6月—7月期间国内,国际,体育,社会,娱乐等18个频道的新闻数据,提供URL和正文信息。

数据格式

<doc>

<url>页面URL</url>

<docno>页面ID</docno>

<contenttitle>页面标题</contenttitle>

<content>页面内容</content>

</doc>

注意:content字段去除了HTML标签,保存的是新闻正文文本

数据文件

搜狐新闻数据区根据文件格式和数据规模细分为以下几种:

  • 迷你版(样例数据, 110KB):tar.gz格式,zip格式

  • 完整版(648MB):tar.gz格式,zip格式

  • 历史版本:2008版(6KB):完整版(同时提供硬盘拷贝,65GB):tar.gz格式

数据预处理

提取中文内容

原始数据中包含完整的html文件,所以需要提取其中的中文内容,通常提取其中<content>标签包含的内容即可。

tar -zxvf news_sohusite_xml.full.tar.gz
cat news_sohusite_xml.dat | iconv -f gb18030 -t utf-8 | grep "<content>" > news_sohusite.txt
sed -i "" 's/<content>//g' news_sohusite.txt
sed -i "" 's/<\/content>//g' news_sohusite.txt

其中iconv命令的格式为:

iconv -f encoding [-t encoding] [inputfile]... 

参数含义为:

  • -f encoding :把字符从encoding编码开始转换。
  • -t encoding :把字符转换到encoding编码。
  • -l :列出已知的编码字符集合
  • -o file :指定输出文件
  • -c :忽略输出的非法字符
  • -s :禁止警告信息,但不是错误信息
  • –verbose :显示进度信息
  • -f和-t所能指定的合法字符在-l选项的命令里面都列出来了

中文切词

与处理英文不同,中文没有切词,需要使用jieba进行切词处理。

python -m jieba -d ' ' news_sohusite.txt > news_sohusite_cutword.txt

训练word2vec

完成预处理后,即可以利用gensim库进行训练。

def train_word2vec(filename):
    #模型文件不存在才处理
    if not os.path.exists(word2vec_file):
        sentences = LineSentence(filename)
        #sg=0 使用cbow训练, sg=1对低频词较为敏感
        model = Word2Vec(sentences,
                         size=n_dim, window=5, min_count=2, sg=1, workers=2)
        model.save(word2vec_file)

Word2Vec函数常见的几个参数含义如下:

  • sentences表示需要处理的语料
  • size表示word2vec的维数,一般50-300
  • window表示处理word时的窗口长度
  • min_count表示处理分析的word出现的最小次数
  • sg为1表示使用skip-gram算法,为0为cbow
  • workers表示计算使用的线程数
  • iter表示迭代计算的次数

使用word2vec处理中文

把一个中文句子使用词向量表示的方法。对于类似短信、微博、标题这些长度较短的文字,可以使用各个word的word2vec相加取平均来表示。对训练数据集创建词向量,接着进行比例缩放(scale)。

def buildWordVector(imdb_w2v,text, size):
    vec = np.zeros(size).reshape((1, size))
    count = 0.
    #print text
    for word in text.split():
        #print word
        try:
            vec += imdb_w2v[word].reshape((1, size))
            count += 1.
        except KeyError:
            print word
            continue
    if count != 0:
        vec /= count
    return vec

当需要把中文数据集X转换成word2vec,可以使用如下方式。

#加载训练好的词向量模型
model = Word2Vec.load(word2vec_file)

x_vecs = np.concatenate([buildWordVector(model,z, n_dim) for z in x])
x_vecs = scale(x_vecs)

测试效果

下面我们测试生成的word2vec模型的质量。

寻找近义词

寻找近义词是word2vec的一个应用场景。

百度的近义词

print pd.Series(model.most_similar(u'百度'))
0      (网易, 0.844283640385)
1    (搜索引擎, 0.822018146515)
2      (腾讯, 0.774820387363)
3       (搜狗, 0.76777946949)
4      (新浪, 0.760137319565)
5      (奇虎, 0.745484173298)
6      (文库, 0.725166857243)
7    (手机软件, 0.717750906944)
8       (优酷, 0.70574760437)
9      (客户端, 0.70448333025)

微信的近义词

print pd.Series(model.most_similar(u'微信'))
0     (摇一摇, 0.768034994602)
1      (陌陌, 0.763847649097)
2    (网上聊天, 0.751431167126)
3    (聊天工具, 0.731707036495)
4      (盗号, 0.722806692123)
5      (飞聊, 0.715048789978)
6      (手机, 0.706719994545)
7     (发短信, 0.704942345619)
8      (聊天, 0.691777765751)
9    (账号密码, 0.679741084576)

单词运算

word2vec的一个神奇之处就是把文字转换成了数字,数字之间的加减运算,同样适用于word2vec。

足球+明星

print pd.Series(model.most_similar(positive=[u'足球'+u'明星']))
0      (巨星, 0.741350233555)
1    (光芒万丈, 0.727712750435)
2     (和亨利, 0.722848057747)
3      (球星, 0.722578346729)
4       (已贵, 0.71345859766)
5     (格米利, 0.694822609425)
6     (支斯篮, 0.690492749214)
7      (田坛, 0.689639627934)
8      (体坛, 0.689606904984)
9     (竞神锋, 0.684816122055)

球星-明星

print pd.Series(model.most_similar(positive=[u'球星'],negative=[u'明星']))
dtype: object
0    (国际米兰, 0.492849290371)
1      (中锋, 0.480526059866)
2      (球员, 0.479797780514)
3     (上赛季, 0.479528963566)
4      (主帅, 0.479275196791)
5      (球队, 0.477513790131)
6     (德里奇, 0.474446773529)
7     (热那亚, 0.472252100706)
8      (中场, 0.459134191275)
9       (巴萨, 0.45858669281)

比较单词的相似度

比较微信和陌陌

print model.wv.similarity(u'微信', u'陌陌')
0.763847656891

比较男人和坏人

print model.wv.similarity(u'男人', u'坏人')
0.617036796702

训练语料

word2vec的算法是公开的,word2vec模型的质量完全取决于训练语料的质量。目前免费开放的预料不多,中文语料更是凤毛麟角。

使用搜狗新闻预料生成word2vec-图1.png

这里推荐使用搜狗实验室的中文语料,对应的网址为:

http://www.sogou.com/labs/resource/cs.php

通常使用”搜狐新闻数据”即可,该数据来自搜狐新闻2012年6月—7月期间国内,国际,体育,社会,娱乐等18个频道的新闻数据,提供URL和正文信息。

数据格式

<doc>

<url>页面URL</url>

<docno>页面ID</docno>

<contenttitle>页面标题</contenttitle>

<content>页面内容</content>

</doc>

注意:content字段去除了HTML标签,保存的是新闻正文文本

数据文件

搜狐新闻数据区根据文件格式和数据规模细分为以下几种:

  • 迷你版(样例数据, 110KB):tar.gz格式,zip格式

  • 完整版(648MB):tar.gz格式,zip格式

  • 历史版本:2008版(6KB):完整版(同时提供硬盘拷贝,65GB):tar.gz格式

数据预处理

提取中文内容

原始数据中包含完整的html文件,所以需要提取其中的中文内容,通常提取其中<content>标签包含的内容即可。

tar -zxvf news_sohusite_xml.full.tar.gz
cat news_sohusite_xml.dat | iconv -f gb18030 -t utf-8 | grep "<content>" > news_sohusite.txt
sed -i "" 's/<content>//g' news_sohusite.txt
sed -i "" 's/<\/content>//g' news_sohusite.txt

其中iconv命令的格式为:

iconv -f encoding [-t encoding] [inputfile]... 

参数含义为:

  • -f encoding :把字符从encoding编码开始转换。
  • -t encoding :把字符转换到encoding编码。
  • -l :列出已知的编码字符集合
  • -o file :指定输出文件
  • -c :忽略输出的非法字符
  • -s :禁止警告信息,但不是错误信息
  • –verbose :显示进度信息
  • -f和-t所能指定的合法字符在-l选项的命令里面都列出来了

中文切词

与处理英文不同,中文没有切词,需要使用jieba进行切词处理。

python -m jieba -d ' ' news_sohusite.txt > news_sohusite_cutword.txt

训练word2vec

完成预处理后,即可以利用gensim库进行训练。

def train_word2vec(filename):
    #模型文件不存在才处理
    if not os.path.exists(word2vec_file):
        sentences = LineSentence(filename)
        #sg=0 使用cbow训练, sg=1对低频词较为敏感
        model = Word2Vec(sentences,
                         size=n_dim, window=5, min_count=2, sg=1, workers=2)
        model.save(word2vec_file)

Word2Vec函数常见的几个参数含义如下:

  • sentences表示需要处理的语料
  • size表示word2vec的维数,一般50-300
  • window表示处理word时的窗口长度
  • min_count表示处理分析的word出现的最小次数
  • sg为1表示使用skip-gram算法,为0为cbow
  • workers表示计算使用的线程数
  • iter表示迭代计算的次数

使用word2vec处理中文

把一个中文句子使用词向量表示的方法。对于类似短信、微博、标题这些长度较短的文字,可以使用各个word的word2vec相加取平均来表示。对训练数据集创建词向量,接着进行比例缩放(scale)。

def buildWordVector(imdb_w2v,text, size):
    vec = np.zeros(size).reshape((1, size))
    count = 0.
    #print text
    for word in text.split():
        #print word
        try:
            vec += imdb_w2v[word].reshape((1, size))
            count += 1.
        except KeyError:
            print word
            continue
    if count != 0:
        vec /= count
    return vec

当需要把中文数据集X转换成word2vec,可以使用如下方式。

#加载训练好的词向量模型
model = Word2Vec.load(word2vec_file)

x_vecs = np.concatenate([buildWordVector(model,z, n_dim) for z in x])
x_vecs = scale(x_vecs)

测试效果

下面我们测试生成的word2vec模型的质量。

寻找近义词

寻找近义词是word2vec的一个应用场景。

百度的近义词

print pd.Series(model.most_similar(u'百度'))
0      (网易, 0.844283640385)
1    (搜索引擎, 0.822018146515)
2      (腾讯, 0.774820387363)
3       (搜狗, 0.76777946949)
4      (新浪, 0.760137319565)
5      (奇虎, 0.745484173298)
6      (文库, 0.725166857243)
7    (手机软件, 0.717750906944)
8       (优酷, 0.70574760437)
9      (客户端, 0.70448333025)

微信的近义词

print pd.Series(model.most_similar(u'微信'))
0     (摇一摇, 0.768034994602)
1      (陌陌, 0.763847649097)
2    (网上聊天, 0.751431167126)
3    (聊天工具, 0.731707036495)
4      (盗号, 0.722806692123)
5      (飞聊, 0.715048789978)
6      (手机, 0.706719994545)
7     (发短信, 0.704942345619)
8      (聊天, 0.691777765751)
9    (账号密码, 0.679741084576)

单词运算

word2vec的一个神奇之处就是把文字转换成了数字,数字之间的加减运算,同样适用于word2vec。

足球+明星

print pd.Series(model.most_similar(positive=[u'足球'+u'明星']))
0      (巨星, 0.741350233555)
1    (光芒万丈, 0.727712750435)
2     (和亨利, 0.722848057747)
3      (球星, 0.722578346729)
4       (已贵, 0.71345859766)
5     (格米利, 0.694822609425)
6     (支斯篮, 0.690492749214)
7      (田坛, 0.689639627934)
8      (体坛, 0.689606904984)
9     (竞神锋, 0.684816122055)

球星-明星

print pd.Series(model.most_similar(positive=[u'球星'],negative=[u'明星']))
dtype: object
0    (国际米兰, 0.492849290371)
1      (中锋, 0.480526059866)
2      (球员, 0.479797780514)
3     (上赛季, 0.479528963566)
4      (主帅, 0.479275196791)
5      (球队, 0.477513790131)
6     (德里奇, 0.474446773529)
7     (热那亚, 0.472252100706)
8      (中场, 0.459134191275)
9       (巴萨, 0.45858669281)

比较单词的相似度

比较微信和陌陌

print model.wv.similarity(u'微信', u'陌陌')
0.763847656891

比较男人和坏人

print model.wv.similarity(u'男人', u'坏人')
0.617036796702

NLP是AI安全领域的一个重要支撑技术。本文讲介绍NLP中的Word2Vec模型和Doc2Vec模型。

Word2Vec

Word2Vec是Google在2013年开源的一款将词表征为实数值向量的高效工具,采用的模型有CBOW(Continuous Bag-Of-Words,即连续的词袋模型)和Skip-Gram 两种。Word2Vec通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度。因此,Word2Vec 输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等。

image.png

CBOW和Skip-gram原理图

CBOW模型能够根据输入周围n-1个词来预测出这个词本身,而Skip-gram模型能够根据词本身来预测周围有哪些词。也就是说,CBOW模型的输入是某个词A周围的n个单词的词向量之和,输出是词A本身的词向量,而Skip-gram模型的输入是词A本身,输出是词A周围的n个单词的词向量。Word2Vec最常用的开源实现之一就是gensim,网址为:

http://radimrehurek.com/gensim/

gensim的安装非常简单:

pip install –upgrade gensim

gensim的使用非常简洁,加载数据和训练数据可以合并,训练好模型后就可以按照单词获取对应的向量表示:

sentences = [['first', 'sentence'], ['second', 'sentence']]model = gensim.models.Word2Vec(sentences, min_count=1)print model['first']

其中Word2Vec有很多可以影响训练速度和质量的参数。第一个参数可以对字典做截断,少于min_count次数的单词会被丢弃掉, 默认值为5:

model = Word2Vec(sentences, min_count=10)

另外一个是神经网络的隐藏层的单元数,推荐值为几十到几百。事实上Word2Vec参数的个数也与神经网络的隐藏层的单元数相同,比如:

size=200

那么训练得到的Word2Vec参数个数也是200:

model = Word2Vec(sentences, size=200)

以处理IMDB数据集为例,初始化Word2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=gensim.models.Word2Vec(size=200, window=8, min_count=10, iter=10, workers=cores)

其中当前计算机的cpu个数可以使用multiprocessing获取:

cores=multiprocessing.cpu_count()

创建字典并开始训练获取Word2Vec。gensim的官方文档中强调增加训练次数可以提高生成的Word2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_testmodel.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Word2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取单词“love”的Word2Vec就可以使用如下形式:

model[“love”]

Word2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。通过遍历一段英文,逐次获取每个单词对应的Word2Vec,连接起来就可以获得该英文段落对应的Word2Vec:

def getVecsByWord2Vec(model, corpus, size):
    x=[]
    for text in corpus:
        xx = []
        for i, vv in enumerate(text):
            try:
                xx.append(model[vv].reshape((1,size)))
            except KeyError:
                continue
        x = np.concatenate(xx)
    x=np.array(x, dtype='float')
    return x

需要注意的是,出于性能的考虑,我们将出现少于10次数的单词会被丢弃掉,所以存在这种情况,就是一部分单词找不到对应的Word2Vec,所以需要捕捉这个异常,通常使用python的KeyError异常捕捉即可。

Doc2Vec

基于上述的Word2Vec的方法,Quoc Le 和Tomas Mikolov又给出了Doc2Vec的训练方法。如下图所示,其原理与Word2Vec相同,分为Distributed Memory (DM) 和Distributed Bag of Words (DBOW)。

image.png

DM和DBOW原理图

以处理IMDB数据集为例,初始化Doc2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=Doc2Vec(dm=0, dbow_words=1, size=max_features, window=8, min_count=10, iter=10, workers=cores)

其中需要强调的是,dm为使用的算法,默认为1,表明使用DM算法,设置为0表明使用DBOW算法,通常使用默认配置即可,比如: model = gensim.models.Doc2Vec.Doc2Vec(size=50, min_count=2, iter=10)与Word2Vec不同的地方是,Doc2Vec处理的每个英文段落,需要使用一个唯一的标识标记,并且使用一种特殊定义的数据格式保存需要处理的英文段落,这种数据格式定义如下:

SentimentDocument = namedtuple('SentimentDocument', 'words tags')

其中SentimentDocument可以理解为这种格式的名称,也可以理解为这种对象的名称,words会保存英文段落,并且是以单词和符合列表的形式保存,tags就是我们说的保存的唯一标识。最简单的一种实现就是依次给每个英文段落编号,训练数据集的标记为“TRAIN数字”,训练数据集的标记为“TEST数字”:

def labelizeReviews(reviews, label_type):
    labelized = []
    for i, v in enumerate(reviews):
        label = '%s_%s' % (label_type, i)
        labelized.append(SentimentDocument(v, [label]))
    return labelized

创建字典并开始训练获取Doc2Vec。与Word2Vec的情况一样,gensim的官方文档中强调增加训练次数可以提高生成的Doc2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_test
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Doc2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取段落“I love tensorflow”的Doc2Vec就可以使用如下形式:

model.docvecs[”I love tensorflow”]

一个典型的doc2ver展开为向量形式,内容如下所示,为了显示方便只展示了其中一部分维度的数据:

array([ 0.02664499, 0.00475204, -0.03981256, 0.03796276, -0.03206162, 0.10963056, -0.04897128, 0.00151982, -0.03258783, 0.04711508, -0.00667155, -0.08523653, -0.02975186, 0.00166316, 0.01915652, -0.03415785, -0.05794788, 0.05110953, 0.01623618, -0.00512495, -0.06385455, -0.0151557 , 0.00365376, 0.03015811, 0.0229462 , 0.03176891, 0.01117626, -0.00743352, 0.02030453, -0.05072152, -0.00498496, 0.00151227, 0.06122205, -0.01811385, -0.01715777, 0.04883198, 0.03925886, -0.03568915, 0.00805744, 0.01654406, -0.05160677, 0.0119908 , -0.01527433, 0.02209963, -0.10316766, -0.01069367, -0.02432527, 0.00761799, 0.02763799, -0.04288232], dtype=float32)

Doc2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。以英文段落为单位,通过遍历训练数据集和测试数据集,逐次获取每个英文段落对应的Doc2Vec,这里的英文段落就可以理解为数据集中针对电影的一段评价:

def getVecs(model, corpus, size):
    vecs = [np.array(model.docvecs[z.tags[0]]).reshape((1, size)) for z in corpus]
    return np.array(np.concatenate(vecs),dtype='float')

训练Word2Vec和Doc2Vec是非常费时费力的过程,调试阶段会频繁更换分类算法以及修改分类算法参数调优,为了提高效率,可以把之前训练得到的Word2Vec和Doc2Vec模型保存成文件形式,以Doc2Vec为例,使用model.save函数把训练后的结果保存在本地硬盘上,运行程序时,在初始化Doc2Vec对象之前,可以先判断本地硬盘是否存在模型文件,如果存在就直接读取模型文件初始化Doc2Vec对象,反之则需要训练数据:

if os.path.exists(doc2ver_bin):
    print "Find cache file %s" % doc2ver_bin
    model=Doc2Vec.load(doc2ver_bin)
else:
    model=Doc2Vec(size=max_features, window=5, min_count=2, workers=cores,iter=40)
    model.build_vocab(x))
    model.train(x, total_examples=model.corpus_count, epochs=model.iter)
    model.save(doc2ver_bin)

截图.png

有兴趣的读者可以关注我的AI安全书籍(京东有售)以及我的公众号 兜哥带你学安全

image.pngimage.png

image.png

NLP是AI安全领域的一个重要支撑技术。本文讲介绍NLP中的Word2Vec模型和Doc2Vec模型。

Word2Vec

Word2Vec是Google在2013年开源的一款将词表征为实数值向量的高效工具,采用的模型有CBOW(Continuous Bag-Of-Words,即连续的词袋模型)和Skip-Gram 两种。Word2Vec通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度。因此,Word2Vec 输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等。

image.png

CBOW和Skip-gram原理图

CBOW模型能够根据输入周围n-1个词来预测出这个词本身,而Skip-gram模型能够根据词本身来预测周围有哪些词。也就是说,CBOW模型的输入是某个词A周围的n个单词的词向量之和,输出是词A本身的词向量,而Skip-gram模型的输入是词A本身,输出是词A周围的n个单词的词向量。Word2Vec最常用的开源实现之一就是gensim,网址为:

http://radimrehurek.com/gensim/

gensim的安装非常简单:

pip install –upgrade gensim

gensim的使用非常简洁,加载数据和训练数据可以合并,训练好模型后就可以按照单词获取对应的向量表示:

sentences = [['first', 'sentence'], ['second', 'sentence']]model = gensim.models.Word2Vec(sentences, min_count=1)print model['first']

其中Word2Vec有很多可以影响训练速度和质量的参数。第一个参数可以对字典做截断,少于min_count次数的单词会被丢弃掉, 默认值为5:

model = Word2Vec(sentences, min_count=10)

另外一个是神经网络的隐藏层的单元数,推荐值为几十到几百。事实上Word2Vec参数的个数也与神经网络的隐藏层的单元数相同,比如:

size=200

那么训练得到的Word2Vec参数个数也是200:

model = Word2Vec(sentences, size=200)

以处理IMDB数据集为例,初始化Word2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=gensim.models.Word2Vec(size=200, window=8, min_count=10, iter=10, workers=cores)

其中当前计算机的cpu个数可以使用multiprocessing获取:

cores=multiprocessing.cpu_count()

创建字典并开始训练获取Word2Vec。gensim的官方文档中强调增加训练次数可以提高生成的Word2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_testmodel.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Word2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取单词“love”的Word2Vec就可以使用如下形式:

model[“love”]

Word2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。通过遍历一段英文,逐次获取每个单词对应的Word2Vec,连接起来就可以获得该英文段落对应的Word2Vec:

def getVecsByWord2Vec(model, corpus, size):
    x=[]
    for text in corpus:
        xx = []
        for i, vv in enumerate(text):
            try:
                xx.append(model[vv].reshape((1,size)))
            except KeyError:
                continue
        x = np.concatenate(xx)
    x=np.array(x, dtype='float')
    return x

需要注意的是,出于性能的考虑,我们将出现少于10次数的单词会被丢弃掉,所以存在这种情况,就是一部分单词找不到对应的Word2Vec,所以需要捕捉这个异常,通常使用python的KeyError异常捕捉即可。

Doc2Vec

基于上述的Word2Vec的方法,Quoc Le 和Tomas Mikolov又给出了Doc2Vec的训练方法。如下图所示,其原理与Word2Vec相同,分为Distributed Memory (DM) 和Distributed Bag of Words (DBOW)。

image.png

DM和DBOW原理图

以处理IMDB数据集为例,初始化Doc2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=Doc2Vec(dm=0, dbow_words=1, size=max_features, window=8, min_count=10, iter=10, workers=cores)

其中需要强调的是,dm为使用的算法,默认为1,表明使用DM算法,设置为0表明使用DBOW算法,通常使用默认配置即可,比如: model = gensim.models.Doc2Vec.Doc2Vec(size=50, min_count=2, iter=10)与Word2Vec不同的地方是,Doc2Vec处理的每个英文段落,需要使用一个唯一的标识标记,并且使用一种特殊定义的数据格式保存需要处理的英文段落,这种数据格式定义如下:

SentimentDocument = namedtuple('SentimentDocument', 'words tags')

其中SentimentDocument可以理解为这种格式的名称,也可以理解为这种对象的名称,words会保存英文段落,并且是以单词和符合列表的形式保存,tags就是我们说的保存的唯一标识。最简单的一种实现就是依次给每个英文段落编号,训练数据集的标记为“TRAIN数字”,训练数据集的标记为“TEST数字”:

def labelizeReviews(reviews, label_type):
    labelized = []
    for i, v in enumerate(reviews):
        label = '%s_%s' % (label_type, i)
        labelized.append(SentimentDocument(v, [label]))
    return labelized

创建字典并开始训练获取Doc2Vec。与Word2Vec的情况一样,gensim的官方文档中强调增加训练次数可以提高生成的Doc2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_test
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Doc2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取段落“I love tensorflow”的Doc2Vec就可以使用如下形式:

model.docvecs[”I love tensorflow”]

一个典型的doc2ver展开为向量形式,内容如下所示,为了显示方便只展示了其中一部分维度的数据:

array([ 0.02664499, 0.00475204, -0.03981256, 0.03796276, -0.03206162, 0.10963056, -0.04897128, 0.00151982, -0.03258783, 0.04711508, -0.00667155, -0.08523653, -0.02975186, 0.00166316, 0.01915652, -0.03415785, -0.05794788, 0.05110953, 0.01623618, -0.00512495, -0.06385455, -0.0151557 , 0.00365376, 0.03015811, 0.0229462 , 0.03176891, 0.01117626, -0.00743352, 0.02030453, -0.05072152, -0.00498496, 0.00151227, 0.06122205, -0.01811385, -0.01715777, 0.04883198, 0.03925886, -0.03568915, 0.00805744, 0.01654406, -0.05160677, 0.0119908 , -0.01527433, 0.02209963, -0.10316766, -0.01069367, -0.02432527, 0.00761799, 0.02763799, -0.04288232], dtype=float32)

Doc2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。以英文段落为单位,通过遍历训练数据集和测试数据集,逐次获取每个英文段落对应的Doc2Vec,这里的英文段落就可以理解为数据集中针对电影的一段评价:

def getVecs(model, corpus, size):
    vecs = [np.array(model.docvecs[z.tags[0]]).reshape((1, size)) for z in corpus]
    return np.array(np.concatenate(vecs),dtype='float')

训练Word2Vec和Doc2Vec是非常费时费力的过程,调试阶段会频繁更换分类算法以及修改分类算法参数调优,为了提高效率,可以把之前训练得到的Word2Vec和Doc2Vec模型保存成文件形式,以Doc2Vec为例,使用model.save函数把训练后的结果保存在本地硬盘上,运行程序时,在初始化Doc2Vec对象之前,可以先判断本地硬盘是否存在模型文件,如果存在就直接读取模型文件初始化Doc2Vec对象,反之则需要训练数据:

if os.path.exists(doc2ver_bin):
    print "Find cache file %s" % doc2ver_bin
    model=Doc2Vec.load(doc2ver_bin)
else:
    model=Doc2Vec(size=max_features, window=5, min_count=2, workers=cores,iter=40)
    model.build_vocab(x))
    model.train(x, total_examples=model.corpus_count, epochs=model.iter)
    model.save(doc2ver_bin)

有兴趣的读者可以关注我的AI安全书籍(京东有售)以及我的公众号 兜哥带你学安全

image.pngimage.png

image.png

NLP是AI安全领域的一个重要支撑技术。本文讲介绍NLP中的Word2Vec模型和Doc2Vec模型。

Word2Vec

Word2Vec是Google在2013年开源的一款将词表征为实数值向量的高效工具,采用的模型有CBOW(Continuous Bag-Of-Words,即连续的词袋模型)和Skip-Gram 两种。Word2Vec通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度。因此,Word2Vec 输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等。

image.png

CBOW和Skip-gram原理图

CBOW模型能够根据输入周围n-1个词来预测出这个词本身,而Skip-gram模型能够根据词本身来预测周围有哪些词。也就是说,CBOW模型的输入是某个词A周围的n个单词的词向量之和,输出是词A本身的词向量,而Skip-gram模型的输入是词A本身,输出是词A周围的n个单词的词向量。Word2Vec最常用的开源实现之一就是gensim,网址为:

http://radimrehurek.com/gensim/

gensim的安装非常简单:

pip install –upgrade gensim

gensim的使用非常简洁,加载数据和训练数据可以合并,训练好模型后就可以按照单词获取对应的向量表示:

sentences = [['first', 'sentence'], ['second', 'sentence']]model = gensim.models.Word2Vec(sentences, min_count=1)print model['first']

其中Word2Vec有很多可以影响训练速度和质量的参数。第一个参数可以对字典做截断,少于min_count次数的单词会被丢弃掉, 默认值为5:

model = Word2Vec(sentences, min_count=10)

另外一个是神经网络的隐藏层的单元数,推荐值为几十到几百。事实上Word2Vec参数的个数也与神经网络的隐藏层的单元数相同,比如:

size=200

那么训练得到的Word2Vec参数个数也是200:

model = Word2Vec(sentences, size=200)

以处理IMDB数据集为例,初始化Word2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=gensim.models.Word2Vec(size=200, window=8, min_count=10, iter=10, workers=cores)

其中当前计算机的cpu个数可以使用multiprocessing获取:

cores=multiprocessing.cpu_count()

创建字典并开始训练获取Word2Vec。gensim的官方文档中强调增加训练次数可以提高生成的Word2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_testmodel.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Word2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取单词“love”的Word2Vec就可以使用如下形式:

model[“love”]

Word2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。通过遍历一段英文,逐次获取每个单词对应的Word2Vec,连接起来就可以获得该英文段落对应的Word2Vec:

def getVecsByWord2Vec(model, corpus, size):
    x=[]
    for text in corpus:
        xx = []
        for i, vv in enumerate(text):
            try:
                xx.append(model[vv].reshape((1,size)))
            except KeyError:
                continue
        x = np.concatenate(xx)
    x=np.array(x, dtype='float')
    return x

需要注意的是,出于性能的考虑,我们将出现少于10次数的单词会被丢弃掉,所以存在这种情况,就是一部分单词找不到对应的Word2Vec,所以需要捕捉这个异常,通常使用python的KeyError异常捕捉即可。

Doc2Vec

基于上述的Word2Vec的方法,Quoc Le 和Tomas Mikolov又给出了Doc2Vec的训练方法。如下图所示,其原理与Word2Vec相同,分为Distributed Memory (DM) 和Distributed Bag of Words (DBOW)。

image.png

DM和DBOW原理图

以处理IMDB数据集为例,初始化Doc2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次数的单词会被丢弃掉,迭代计算次数为10次,同时并发线程数与当前计算机的cpu个数相同:

model=Doc2Vec(dm=0, dbow_words=1, size=max_features, window=8, min_count=10, iter=10, workers=cores)

其中需要强调的是,dm为使用的算法,默认为1,表明使用DM算法,设置为0表明使用DBOW算法,通常使用默认配置即可,比如: model = gensim.models.Doc2Vec.Doc2Vec(size=50, min_count=2, iter=10)与Word2Vec不同的地方是,Doc2Vec处理的每个英文段落,需要使用一个唯一的标识标记,并且使用一种特殊定义的数据格式保存需要处理的英文段落,这种数据格式定义如下:

SentimentDocument = namedtuple('SentimentDocument', 'words tags')

其中SentimentDocument可以理解为这种格式的名称,也可以理解为这种对象的名称,words会保存英文段落,并且是以单词和符合列表的形式保存,tags就是我们说的保存的唯一标识。最简单的一种实现就是依次给每个英文段落编号,训练数据集的标记为“TRAIN数字”,训练数据集的标记为“TEST数字”:

def labelizeReviews(reviews, label_type):
    labelized = []
    for i, v in enumerate(reviews):
        label = '%s_%s' % (label_type, i)
        labelized.append(SentimentDocument(v, [label]))
    return labelized

创建字典并开始训练获取Doc2Vec。与Word2Vec的情况一样,gensim的官方文档中强调增加训练次数可以提高生成的Doc2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:

x=x_train+x_test
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)

经过训练后,Doc2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取段落“I love tensorflow”的Doc2Vec就可以使用如下形式:

model.docvecs[”I love tensorflow”]

一个典型的doc2ver展开为向量形式,内容如下所示,为了显示方便只展示了其中一部分维度的数据:

array([ 0.02664499, 0.00475204, -0.03981256, 0.03796276, -0.03206162, 0.10963056, -0.04897128, 0.00151982, -0.03258783, 0.04711508, -0.00667155, -0.08523653, -0.02975186, 0.00166316, 0.01915652, -0.03415785, -0.05794788, 0.05110953, 0.01623618, -0.00512495, -0.06385455, -0.0151557 , 0.00365376, 0.03015811, 0.0229462 , 0.03176891, 0.01117626, -0.00743352, 0.02030453, -0.05072152, -0.00498496, 0.00151227, 0.06122205, -0.01811385, -0.01715777, 0.04883198, 0.03925886, -0.03568915, 0.00805744, 0.01654406, -0.05160677, 0.0119908 , -0.01527433, 0.02209963, -0.10316766, -0.01069367, -0.02432527, 0.00761799, 0.02763799, -0.04288232], dtype=float32)

Doc2Vec的维度与之前设置的神经网络的隐藏层的单元数相同为200,也就是说是一个长度为200的一维向量。以英文段落为单位,通过遍历训练数据集和测试数据集,逐次获取每个英文段落对应的Doc2Vec,这里的英文段落就可以理解为数据集中针对电影的一段评价:

def getVecs(model, corpus, size):
    vecs = [np.array(model.docvecs[z.tags[0]]).reshape((1, size)) for z in corpus]
    return np.array(np.concatenate(vecs),dtype='float')

训练Word2Vec和Doc2Vec是非常费时费力的过程,调试阶段会频繁更换分类算法以及修改分类算法参数调优,为了提高效率,可以把之前训练得到的Word2Vec和Doc2Vec模型保存成文件形式,以Doc2Vec为例,使用model.save函数把训练后的结果保存在本地硬盘上,运行程序时,在初始化Doc2Vec对象之前,可以先判断本地硬盘是否存在模型文件,如果存在就直接读取模型文件初始化Doc2Vec对象,反之则需要训练数据:

if os.path.exists(doc2ver_bin):
    print "Find cache file %s" % doc2ver_bin
    model=Doc2Vec.load(doc2ver_bin)
else:
    model=Doc2Vec(size=max_features, window=5, min_count=2, workers=cores,iter=40)
    model.build_vocab(x))
    model.train(x, total_examples=model.corpus_count, epochs=model.iter)
    model.save(doc2ver_bin)

有兴趣的读者可以关注我的AI安全书籍(京东有售)以及我的公众号 兜哥带你学安全

image.pngimage.png

image.png

NLP是AI安全领域的一个重要支撑技术。本文讲介绍NLP中的词袋和TF-IDF模型。

词袋模型

文本特征提取有两个非常重要的模型:

  • 词集模型:单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个。

  • 词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。

两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。

假设我们要对一篇文章进行特征化,最常见的方式就是词袋。

导入相关的函数库:

from sklearn.feature_extraction.text import CountVectorizer

实例化分词对象:

vectorizer = CountVectorizer(min_df=1)

vectorizer                    

CountVectorizer(analyzer=...'word', binary=False, decode_error=...'strict',

        dtype=<... 'numpy.int64'>, encoding=...'utf-8', input=...'content',

        lowercase=True, max_df=1.0, max_features=None, min_df=1,

        ngram_range=(1, 1), preprocessor=None, stop_words=None,

        strip_accents=None, token_pattern=...'(?u)\\b\\w\\w+\\b',

        tokenizer=None, vocabulary=None)

将文本进行词袋处理:

corpus = [

...     'This is the first document.',

...     'This is the second second document.',

...     'And the third one.',

...     'Is this the first document?',

... ]

X = vectorizer.fit_transform(corpus)

X                             

<4x9 sparse matrix of type '<... 'numpy.int64'>'

    with 19 stored elements in Compressed Sparse ... format>

获取对应的特征名称:

>>> vectorizer.get_feature_names() == (

...     ['and', 'document', 'first', 'is', 'one',

...      'second', 'the', 'third', 'this'])

True

获取词袋数据,至此我们已经完成了词袋化:

>>> X.toarray()          

array([[0, 1, 1, 1, 0, 0, 1, 0, 1],

       [0, 1, 0, 1, 0, 2, 1, 0, 1],

       [1, 0, 0, 0, 1, 0, 1, 1, 0],

       [0, 1, 1, 1, 0, 0, 1, 0, 1]]...)

但是如何可以使用现有的词袋的特征,对其他文本进行特征提取呢?我们定义词袋的特征空间叫做词汇表vocabulary:

vocabulary=vectorizer.vocabulary_

针对其他文本进行词袋处理时,可以直接使用现有的词汇表:

 new_vectorizer = CountVectorizer(min_df=1, vocabulary=vocabulary)

CountVectorize函数比较重要的几个参数为:

  • decode_error,处理解码失败的方式,分为‘strict’、‘ignore’、‘replace’三种方式。

  • strip_accents,在预处理步骤中移除重音的方式。

  • max_features,词袋特征个数的最大值。

  • stop_words,判断word结束的方式。

  • max_df,df最大值。

  • min_df,df最小值 。

  • binary,默认为False,当与TF-IDF结合使用时需要设置为True。

本例中处理的数据集均为英文,所以针对解码失败直接忽略,使用ignore方式,stop_words的方式使用english,strip_accents方式为ascii方式。

TF-IDF模型

文本处理领域还有一种特征提取方法,叫做TF-IDF模型(term frequency–inverse document frequency,词频与逆向文件频率)。TF-IDF是一种统计方法,用以评估某一字词对于一个文件集或一个语料库的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。TF-IDF加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。

TF-IDF的主要思想是,如果某个词或短语在一篇文章中出现的频率TF(Term Frequency,词频),词频高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。TF-IDF实际上是:TF * IDF。TF表示词条在文档d中出现的频率。IDF(inverse document frequency,逆向文件频率)的主要思想是:如果包含词条t的文档越少,也就是n越小,IDF越大,则说明词条t具有很好的类别区分能力。如果某一类文档C中包含词条t的文档数为m,而其他类包含t的文档总数为k,显然所有包含t的文档数n=m+k,当m大的时候,n也大,按照IDF公式得到的IDF的值会小,就说明该词条t类别区分能力不强。但是实际上,如果一个词条在一个类的文档中频繁出现,则说明该词条能够很好代表这个类的文本的特征,这样的词条应该给它们赋予较高的权重,并选来作为该类文本的特征词以区别与其他类文档。

在Scikit-Learn中实现了TF-IDF算法,实例化TfidfTransformer即可:

from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer(smooth_idf=False)

transformer    

TfidfTransformer(norm=...'l2', smooth_idf=False, sublinear_tf=False, use_idf=True)

TF-IDF模型通常和词袋模型配合使用,对词袋模型生成的数组进一步处理:

>>> counts = [[3, 0, 1],

...           [2, 0, 0],

...           [3, 0, 0],

...           [4, 0, 0],

...           [3, 2, 0],

...           [3, 0, 2]]

...

 >>> tfidf = transformer.fit_transform(counts)

>>> tfidf                         

<6x3 sparse matrix of type '<... 'numpy.float64'>'     with 9 stored elements in Compressed Sparse ... format> 

>>> tfidf.toarray()                         

array([[ 0.81940995,  0.        ,  0.57320793],       

[ 1.        ,  0.        ,  0.        ],      

[ 1.        ,  0.        ,  0.        ],  

[ 1.        ,  0.        ,  0.        ],       

[ 0.47330339,  0.88089948,  0.        ],       

[ 0.58149261,  0.        ,  0.81355169]])

词汇表模型

词袋模型可以很好的表现文本由哪些单词组成,但是却无法表达出单词之间的前后关系,于是人们借鉴了词袋模型的思想,使用生成的词汇表对原有句子按照单词逐个进行编码。TensorFlow默认支持了这种模型:

tf.contrib.learn.preprocessing.VocabularyProcessor (

                                          max_document_length,    

                                          min_frequency=0,

                                          vocabulary=None,

                                          tokenizer_fn=None)

其中各个参数的含义为:

  • max_document_length:,文档的最大长度。如果文本的长度大于最大长度,那么它会被剪切,反之则用0填充。

  • min_frequency,词频的最小值,出现次数小于最小词频则不会被收录到词表中。

  • vocabulary,CategoricalVocabulary 对象。

  • tokenizer_fn,分词函数。

假设有如下句子需要处理:

x_text =[

    'i love you',

    'me too'

]

基于以上句子生成词汇表,并对’i me too’这句话进行编码:

vocab_processor = learn.preprocessing.VocabularyProcessor(max_document_length)

vocab_processor.fit(x_text)

print next(vocab_processor.transform(['i me too'])).tolist()

x = np.array(list(vocab_processor.fit_transform(x_text)))

print x

运行程序,x_text使用词汇表编码后的数据为:

 [[1 2 3 0]

 [4 5 0 0]]

‘i me too’这句话编码的结果为:

[1, 4, 5, 0]

整个过程如下图所示。

截图.png

有兴趣的读者可以关注我的AI安全书籍(京东有售)以及我的公众号 兜哥带你学安全

image.pngimage.png

image.png

NLP是AI安全领域的一个重要支撑技术。本文讲介绍NLP中的词袋和TF-IDF模型。

词袋模型

文本特征提取有两个非常重要的模型:

  • 词集模型:单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个。

  • 词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。

两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。

假设我们要对一篇文章进行特征化,最常见的方式就是词袋。

导入相关的函数库:

from sklearn.feature_extraction.text import CountVectorizer

实例化分词对象:

vectorizer = CountVectorizer(min_df=1)

vectorizer                    

CountVectorizer(analyzer=...'word', binary=False, decode_error=...'strict',

        dtype=<... 'numpy.int64'>, encoding=...'utf-8', input=...'content',

        lowercase=True, max_df=1.0, max_features=None, min_df=1,

        ngram_range=(1, 1), preprocessor=None, stop_words=None,

        strip_accents=None, token_pattern=...'(?u)\\b\\w\\w+\\b',

        tokenizer=None, vocabulary=None)

将文本进行词袋处理:

corpus = [

...     'This is the first document.',

...     'This is the second second document.',

...     'And the third one.',

...     'Is this the first document?',

... ]

X = vectorizer.fit_transform(corpus)

X                             

<4x9 sparse matrix of type '<... 'numpy.int64'>'

    with 19 stored elements in Compressed Sparse ... format>

获取对应的特征名称:

>>> vectorizer.get_feature_names() == (

...     ['and', 'document', 'first', 'is', 'one',

...      'second', 'the', 'third', 'this'])

True

获取词袋数据,至此我们已经完成了词袋化:

>>> X.toarray()          

array([[0, 1, 1, 1, 0, 0, 1, 0, 1],

       [0, 1, 0, 1, 0, 2, 1, 0, 1],

       [1, 0, 0, 0, 1, 0, 1, 1, 0],

       [0, 1, 1, 1, 0, 0, 1, 0, 1]]...)

但是如何可以使用现有的词袋的特征,对其他文本进行特征提取呢?我们定义词袋的特征空间叫做词汇表vocabulary:

vocabulary=vectorizer.vocabulary_

针对其他文本进行词袋处理时,可以直接使用现有的词汇表:

 new_vectorizer = CountVectorizer(min_df=1, vocabulary=vocabulary)

CountVectorize函数比较重要的几个参数为:

  • decode_error,处理解码失败的方式,分为‘strict’、‘ignore’、‘replace’三种方式。

  • strip_accents,在预处理步骤中移除重音的方式。

  • max_features,词袋特征个数的最大值。

  • stop_words,判断word结束的方式。

  • max_df,df最大值。

  • min_df,df最小值 。

  • binary,默认为False,当与TF-IDF结合使用时需要设置为True。

本例中处理的数据集均为英文,所以针对解码失败直接忽略,使用ignore方式,stop_words的方式使用english,strip_accents方式为ascii方式。

TF-IDF模型

文本处理领域还有一种特征提取方法,叫做TF-IDF模型(term frequency–inverse document frequency,词频与逆向文件频率)。TF-IDF是一种统计方法,用以评估某一字词对于一个文件集或一个语料库的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。TF-IDF加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。

TF-IDF的主要思想是,如果某个词或短语在一篇文章中出现的频率TF(Term Frequency,词频),词频高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。TF-IDF实际上是:TF * IDF。TF表示词条在文档d中出现的频率。IDF(inverse document frequency,逆向文件频率)的主要思想是:如果包含词条t的文档越少,也就是n越小,IDF越大,则说明词条t具有很好的类别区分能力。如果某一类文档C中包含词条t的文档数为m,而其他类包含t的文档总数为k,显然所有包含t的文档数n=m+k,当m大的时候,n也大,按照IDF公式得到的IDF的值会小,就说明该词条t类别区分能力不强。但是实际上,如果一个词条在一个类的文档中频繁出现,则说明该词条能够很好代表这个类的文本的特征,这样的词条应该给它们赋予较高的权重,并选来作为该类文本的特征词以区别与其他类文档。

在Scikit-Learn中实现了TF-IDF算法,实例化TfidfTransformer即可:

from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer(smooth_idf=False)

transformer    

TfidfTransformer(norm=...'l2', smooth_idf=False, sublinear_tf=False, use_idf=True)

TF-IDF模型通常和词袋模型配合使用,对词袋模型生成的数组进一步处理:

>>> counts = [[3, 0, 1],

...           [2, 0, 0],

...           [3, 0, 0],

...           [4, 0, 0],

...           [3, 2, 0],

...           [3, 0, 2]]

...

 >>> tfidf = transformer.fit_transform(counts)

>>> tfidf                         

<6x3 sparse matrix of type '<... 'numpy.float64'>'     with 9 stored elements in Compressed Sparse ... format> 

>>> tfidf.toarray()                         

array([[ 0.81940995,  0.        ,  0.57320793],       

[ 1.        ,  0.        ,  0.        ],      

[ 1.        ,  0.        ,  0.        ],  

[ 1.        ,  0.        ,  0.        ],       

[ 0.47330339,  0.88089948,  0.        ],       

[ 0.58149261,  0.        ,  0.81355169]])

词汇表模型

词袋模型可以很好的表现文本由哪些单词组成,但是却无法表达出单词之间的前后关系,于是人们借鉴了词袋模型的思想,使用生成的词汇表对原有句子按照单词逐个进行编码。TensorFlow默认支持了这种模型:

tf.contrib.learn.preprocessing.VocabularyProcessor (

                                          max_document_length,    

                                          min_frequency=0,

                                          vocabulary=None,

                                          tokenizer_fn=None)

其中各个参数的含义为:

  • max_document_length:,文档的最大长度。如果文本的长度大于最大长度,那么它会被剪切,反之则用0填充。

  • min_frequency,词频的最小值,出现次数小于最小词频则不会被收录到词表中。

  • vocabulary,CategoricalVocabulary 对象。

  • tokenizer_fn,分词函数。

假设有如下句子需要处理:

x_text =[

    'i love you',

    'me too'

]

基于以上句子生成词汇表,并对’i me too’这句话进行编码:

vocab_processor = learn.preprocessing.VocabularyProcessor(max_document_length)

vocab_processor.fit(x_text)

print next(vocab_processor.transform(['i me too'])).tolist()

x = np.array(list(vocab_processor.fit_transform(x_text)))

print x

运行程序,x_text使用词汇表编码后的数据为:

 [[1 2 3 0]

 [4 5 0 0]]

‘i me too’这句话编码的结果为:

[1, 4, 5, 0]

整个过程如下图所示。

截图.png

有兴趣的读者可以关注我的AI安全书籍(京东有售)以及我的公众号 兜哥带你学安全

image.pngimage.png

image.png

2008 年,我是看着《我的华为十年》这篇文章进入这家公司的,当时我的总监就是这篇文章的作者家骏。转眼云烟,第一份工作做到了现在。

菜鸟入职

我入职的时候,公司规模远没有现在这么大,北京地区的研发零星分散在中关村的几个写字楼,包括理想国际、普天大厦和银科大厦。

我是在普天大厦入职的,记得当时是 12 月份,第一次冬天来北京的我,领会到了啥叫冰雪两重天。

在武汉是没有暖气的,屋外四度,屋里也是四度。北京是有暖气的,屋外零下十度,屋里起步价二十度。

公司里不少大姐大哥在公司里面穿的和夏天一样,出门套个大棉袄正合适。

我没经验,带着一身毛衣毛裤来的,结果在外面还是冻死,在公司里热死。

我大二的时候开始给华为三康做一些项目,在公司里面是不能上外网,而且都是又重又大的台式机。

入职的时候,行政小哥发给我个笔记本,我当时一激动就问了一句,这个回家加班可以用不?

行政小哥一脸懵逼,为啥不可以呀。

刚到北京的第一个月我是住在南二环的亲戚家,公司在拥挤的宇宙中心中关村,每天基本我就跟取经一样,天蒙蒙灰就出门,天漆漆黑回家。

第一个任务:防火墙双机上线

每一个自己觉得自己很牛逼的公司,都会有一个精心设计的新员工培训,有的像传销,有的像传教,有的讲情怀,有的忆苦思甜。我对新员工培训唯一的印象就是别在公司抽烟和养宠物,其他随便。可见当初一定有个在公司抽烟又养宠物的人把大佬惹毛了。虽然我不抽烟,但是我想养个小乌龟小金鱼,也只能放弃。

我的第一个任务是做防火墙双机上线,首先感谢组织信任,其次觉得专业好像有点不对口,我是个 rd 呀,虽然我懂点网络,不过也是写过交换机的软件而已。于是我硬着头皮看白皮书,看配置命令,幸好一起升级的还有经理黄姐和一个老员工永校,感觉自己打下手应该问题也不大。第一个任务总不能搞砸嘛,我还蛮认真的画了网络拓扑和配置回滚方案。其实仔细想想,双机以前不就是一个防火墙吗,现在就是再放一台上去呗。

为了不影响业务,我们选择在元旦凌晨上线。上线的过程确实非常顺利,简单描述,就是把新防火墙放上机架,插上电,配置灌进去,接线,搭完收工。十分钟不到搞定了,我都准备撤了。

老员工说,这才哪到哪呀,我们要验证可以自动切换。新防火墙和老防火墙之间有两根心跳线,汇聚层有大概 6 台汇聚层交换机,分别会连到两台防火墙上,要验证诸如心跳线段,上联线段的情况。另外国内众所周知的原因,分为南北网,联通电信互通很差劲,防火墙上联四根运营商的链路,还要测试这几根线路断的情况下的自动切换。于是乎,不停插拔网线,ping 新浪 ping 搜狐。测试完凌晨 4 点了,总算搞得差不多了。我记得我走出普天大厦的时候,居然看到了 2009 年的第一个日出了。

第一个项目:准入系统 BNAC

我的第一个项目是开发准入系统。所谓的准入系统,简单讲,就是上网认证,主机安全检查加上网络权限控制。满足一定安全基线要求的终端才允许接入公司办公网,并且根据不同的部门和职位,赋予不同的网络访问权限。

公司当时已经买了号称全球顶尖的准入系统,不过在易用性和可定制性上差强人意。另外一个原因,我们总监最初在华为就做了第一套准入系统,他对准入的理解非常深刻,从他的角度来看,目前这个国外的准入系统,有线无线不能自动切换,不能和微软的域管理集成,权限管理过于死板,最坑爹是对网络设备有要求,捆绑销售他们的防火墙。

这些在传统企业不是太大问题,但是在互联网公司就是硬伤了。于是我入职前基本就拍下来要自己研发准入系统。他老人家是理解深刻,我理解不深刻呀,从网上搜了个遍,还是一知半解。还好有个老安全工程师志刚领路,介绍了一些厂商进行交流,总算整明白咋回事了。于是开工干活,整个系统分为客户端,策略管理平台,测试管理服务器和防火墙。防火墙由系统部的一个大拿负责写网络控制模块,现在这哥们是我们公司 CDN、流量清洗这些基础设施的负责人。

客户端的主机检查模块由我们另外一个安全工程师负责,他现在也是 BAT 某公司的高 P 了。其他都是我弄的,第一次写网页还有点小兴奋,尤其是自己画 logo,尽显人文修养。我在学校用 delphi 写了系的教师考评系统,在那个年代 dephi + sqlserver 是绝配,access 也面对小 case 也是 ok 的,因此我们考核系统也是 cs 架构的。惯性思维,我的客户端也是用 delphi 写的。为了支持使用 linux 办公的同学,还开发了 linux 客户端。

网页完全是新接触,用了当时比较新的 groovy。这个时期我接触了大量新知识,这些开发语言还是其次,主要是认识了不少网络设备,接入层的从啥 hub 到二层交换机,三层交换机,还有啥无线 AP 和 AC。和部署实施比起来,开发这个阶段是多么美好的回忆,事实上我差不多 3 个月开发完了第一版,为了验证有效性,我们打算在部分办公区部署。于是我们开始杀熟,先在我们部门使用,我待人和气的好脾气也是这个阶段养成的。

我们把我们部门的办公区的接入交换机和汇聚层交换机之间传入了防火墙。所谓的防火墙其实就是台服务器,最早用的是 dell 的 2850。现在看起来 2850 的配置确实差的可怜,四核八G内存,六块七十三 G 的大硬盘,还齁沉齁沉的。我一个人搬它还很费劲,经常要和一个叫大肉的老员工一起搬。

由于当时交换机的机柜就在办公区,我们的防火墙也只能放办公区。别看 2850 配置不行,风扇确极其彪悍,一开机地动山摇,半层楼能听见。经常可以听到旁边部门骂,谁这么缺德把服务器放办公区,还让不让人上班生孩子啥的。

我们公司没有花名这一说,但是我处于怕人知道我真名骂我,我很早就用花名了。差不多那个时候开始叫麦兜了,至少名字这么可爱,大家骂的时候也有所估计,后来岁数大了开始叫兜哥了,这也是我网名的由来。

总被骂,确实也觉得对不起大家,所以一直到现在也待人客气。还好除了吵,基本没出现过断网的问题,偶尔出现过奇葩软件和客户端不兼容的情况,也很快解决了。在那个蠕虫病毒泛滥的年代,我们通过准入系统强制电脑安装杀毒、安装补丁、开启防火墙等等,简直就是功德无量,很多年都没有发生过大面积的病毒感染,一直服役到现在,差不多有 9 年了。我到现在还记得家俊在部门会上说某某厂做准入几百人,我们就搭进去个麦兜。

枪版网工生活

2009 年,全部门的重点就是建设新大厦,所谓的新大厦,就是现在我们叫的老大厦,就是西二旗旁边那个百度大厦。当时网络工程师就三个人,大肉,秀英和永校,人手不够就把我也搭上了。我是革命一块砖,哪里需要新员工哪里搬。

这次真成网络工程师了。小时候觉得工程师很牛逼,工作后发现其实叫网工更合适,就和电工一样。好在安全工程师说起来还有点黑客帝国的感觉,即使简称安工,也感觉和同仁堂的救命神药安宫硫磺一样牛逼哄哄的。不过我们这几位网工可牛逼了,一个是 3com 和华为研发出身的,另外一个是 2008 奥运会的网络建设负责人之一,相比我就是渣渣了,而且还是业余渣渣。

给我的第一个任务是生成全部网络设备的配置,大概是 500 多台有线交换机,200 多台无线 ap 和交换机的配置。当时在奥运会的时候,他们是规划好 IP 地址后,通过一个 java 的程序手工配置参数后生成一个设备的配置,然后通过 securecrt + js 脚本 + 串口把配置文件灌入设备。有多少台设备就要手工配置多少次,不过这个已经比手工写配置文件牛逼很多了。

当时对我的预期是把思科设备的文件改成华为设备的。我对网络设备的配置完全是工作后学的,半桶水都不到,也是这个时期我学会了思科和华为设备的使用和配置方法。我看懂了原来的程序后,发现其实把网络规划体现到电子表格后,通过程序读取数据,可以一次性生成全部配置。而且这都是纯文本的活,用 perl 更合适。于是我重头开始写,差不多一周多业余时间完成了 demo,当时我还写准入在。

中间也出过不少问题,比如关键字写错了,思科的一些命令没改,有些参数写死了,最后又改了几次才能用,虽然被骂的也挺惨,不过最后对我评价是超出预期,大大节省了网工的工作量,唰唰唰 10 分钟可以生成全部配置。尤其是有次网工发现自己电子表格写错了,要是以前他需要一台一台配置去生成,但是我这边重新 run 一下就好了。

那段时间我还在陪大肉升级交换机 OS 和灌配置的过程中自学了 CCNA 和 CCNP,现在也很怀念和大肉在信威大厦里面灌配置的日子。那段时间玩命加班,几乎 3 个月没有咋过双休,最后竣工的时候居然有了 19 天调休。一直到现在,如果面有网路经验的安全工程师时,我总能扯好久,一直可以问傻别人,也是这段时间积累的。

内部安全建设黄金时代

这个时期是我们公司内部安全建设的黄金时代,很大一个原因是我们有了级别非常高的 CIO John Gu。John 长期在国外工作,一直做到几个巨型企业的 CIO。相对国内私企,国外企业对安全重视很多。和 John 不用太多介绍安全的重要性,而是想好怎样做好安全就可以了。为了有更好的视野,我们还挖来了埃森哲的架构师欧阳。大概有 2-3 年的时间,我们都有非常充足的预算的进行安全建设,我也开始带 team。这段时间我的工作才开始接近我理解的安全工程师和甲方安全。

这段时间我比较系统的建设了内部安全体系,从企业杀毒、终端补丁管理、DLP、邮件安全网关、IPS、漏洞扫描器、上网行为审计、APT 检测到终端安全加固、软 token、堡垒机、应用虚拟化、硬盘加密、文件加密等。那个时期,负责互联网公司的国外安全厂商的销售,应该大部分认识我。这段时间,是我安全知识面扩展非常快的一个时期。一直到现在,我跟许多解决方案架构师沟通很顺畅,也是得益于这个时期积累的知识。我后面可以承担 PGM 的工作,有相当一部分原因是我对安全产品需求的感觉,这种感觉的培养其实也来自于我这段经历。

云安全部成立

在很长一段时间,我们没有安全部,安全的职责分散在技术体系下不同部门的几个组里面。早期问题并不大,大家各司其职,但是当公司发展到一定程度后,对外的产品线日趋繁杂,内部的协同配合压力日趋变大。于是在某年某月的某一天,我们几个分散的小组合并成立一个新部门,曰云安全部。人员合并后按照每个人的技能重组团队,我负责基础架构安全的 team,曰 isec,职责范围包括内部网和生产网。我的核心 team 成员也是从那个时候一直和我到现在,现在想想也真不容易。

与内网相比,生产网有趣很多,安全工程师的压力也大很多。物理服务器的数量达到数万甚至数十万,虚拟机以及容器数量起步价也是百万级了,出口带宽几百 G 的机房遍布全球,涉及的产品线更是复杂到令人发指,只要想的到的业务几乎都有,想不到的没准也有。相对内部网,生产网攻击面大很多,毕竟这些业务是 7 乘 24 对数亿网名提供服务的。我们面对的最大挑战就是如何在业务不中断,不损失访问流量的情况下保障业务的安全。因此我们的重点一个是安全加固,一个是入侵检测,其中入侵检测是我很喜欢的一个领域。在国内,入侵检测经常被理解为 IDS/IPS 这样的安全设备。在以 web 浏览访问以及手机 app 访问为主要业务形式的互联网公司,入侵检测覆盖分范围非常广泛。

我首先遇到的一个问题其实不是技术上的,如何衡量我们所做的努力对公司安全状况的贡献。换句话来说,就是如何描述我们做的事的产出。在大多数公司,甲方安全都是地道的成本中心,纯成本消耗。如何证明安全团队的价值是非常重要的,即使是在一个超大型互联网公司。我观察到有些同学其实干的也很苦逼,情绪低落,总是抱怨。确实他负责很多小项目,每个事情看似很重要,但是确实也看不到啥产出,感觉做不做其实也一样。于是明显的恶性循环也产生了,一个事情没做成业绩,就继续做另外一个,结果下一个也没做出成绩,继续做下一个,手上一堆烂尾楼,还要抱怨辛苦没人看到。

在那个时候,某知名漏洞平台还在,上面报的漏洞公司层面还是非常重视的。于是我想到一个重要的衡量指标,就是安全事故的主动发现比例。比如拿到服务器的 webshell,SQL 注入点和敏感文件下载,这些都是影响大且容易量化的。如果能够通过我们开发的入侵检测系统,提高我们主动发现入侵事件的比例,这个贡献是非常容易体现的。我们在相当长的一段时间就是从各个维度想办法提高这些指标,其中印象深刻的就是 webdir 和 dbmon。

webdir&dbmon

webdir 和 dbmon 是我们内部取的名字,简单讲 webdir 分析 web 服务器上的文件,及时发现后门文件,dbmon 分析数据库日志,及时发现 SQL 注入点以及拖库行为。通过这两个项目,我的 team 从一个安全技术团队开始向一个安全产品团队衍变,除了负责应急响应和渗透测试的的安全工程师,开始出现有安全背景的研发工程师以及负责 storm 和 hive 的大数据工程师,人数也开始两位数了。

webdir 在一期的时候,主要是依赖收集的样本提炼的文本规则,简单有效,在部署的初期发现了不少 case,部署的范围主要集中在重点产品线,量级在一万台左右。我们在二期的时候,重点工作是一方面提高检测能力,一方面是减少发现的延时,另外一个方面是全公司部署,这三方面都是为了提高 webshell 的主动发现比例。

在检测能力方面,主要是提高准确率和召回率,关于这两个指标的含义,有兴趣的同学可以看下我机器学习的书,里面用小龙虾和鱼来做了形象的比喻。基于文本特征的 webshell 检测,很难在这两个指标之间做平衡,尤其是我们这种超大规模的公司,即使是每天新增的文件也可能上亿,实验室环境看着还蛮不错的检测效果,误报也会被放大。因此大多数安全工程师的选择就是写极其精准的规则,所谓精准,就是根据搜集的样本写的过于严格苛刻的规则,用于大大降低误报。这导致的结果是,误报确实少了,但是漏报也非常严重。

我们仔细研究了下问题所在,主要是由于 php 语言的高度灵活性,一个很简单的功能可以用多种方式实现,还有不少装逼的语法。单纯在语言文本特征层面做非常吃力。通过调研,我们发现不管文本特征层面如何做绕过我们的检测,最后 webshell 还是要以 php 和 java 的语法来实现,如果我们可以实现 php 和 java 的语法,就可以在更底层提取特征,与黑产进行对抗。

这个思路也一直影响了我们后面的流量分析产品以及基于机器学习的 webshell 识别,不过这个是后话了。这个思路也成为我们二期的主要提升点,当时根据我们搜集的数千样本,挑选了专业的安全产品进行测试对比,我们的两个指标综合领先。我们另外的一个挑战是工程上的,我们仅在国内就有大量的机房,每个机房之间的带宽不尽相同,而且使用率也大不相同,即使是固定的两个机房,带宽使用也有明显的时间特征。

另外互联网公司大多把服务器的性能压榨的非常腻害,运维部门对我们的性能指标限制的非常死,甚至超过一定的 CPU 或者内存就会自动把我们进程挂起甚至 kill。为了尽可能降低服务器的性能消耗,我们使用云模式,负责的语法解析与规则匹配放到云端,服务器上仅需要完成非常简单的处理和上传逻辑。但是几十万个服务器如果因为上线新版本同时出现新文件需要检测,也可能会出现带宽的异常消耗,于是我们也使用了去中心化的部署方式。

一群只玩过单机版 syslog-ng 分析日志的土鳖,一下子可以有上百台服务器,还用上了大型消息队列和自研的沙箱集群,想想确实很有成就感。二期上线后,无论从部署范围还是检测能力上,都上了一个新台阶,并且由于检测技术上的创新以及客观的评测结果,这个项目获得了公司层面的创新奖。在这个项目上另外一个收获是开发服务器端的程序的经验,在一个如此大规模的集群上部署客户端,还要做到性能消耗小,考虑各种异常情况的处理,考虑各种兼容性问题,这些都是干过才能积累的。

dbmon 在一期的时候,依托于公司运维部的 DBA 团队的现有系统,离线分析公司部门产品线托管的 mysql 查询日志。检测的效果确实不理想,一堆暴力破解的报警,仔细一查都是密码过期了。检测的重点没有放到 SQL 这些上,而是更像针对数据库的异常访问检测了,这个其实从实践角度,安全人员很难去定位问题,小同学弄两次就烦了,所以效果一直很差,最后运维系统的同学根本不想看报警了。

二期的时候我们聚焦到 SQL 检测上,相对于 waf 和流量层面,SQL 日志层面做 SQL 注入点检测非常合适,因为在 http 协议层面可以有大量绕过 sql 注入检测的技巧,但是最终还是会落地到可以执行的 SQL 语句,在 SQL 日志层面会大大简化这方面的检测,相对于负责的 WAF 规则,SQL 日志层面上的检测是在黑客难以控制的更底层进行对抗。在这个阶段即使是文本特征的检测,在准确率和召回率上表现已经不错了。

上线效果非常好,同学们对这个也有了信心。集思广益,在三期的时候我们在 SQL 层面尝试了也使用语法而不仅仅是文本规则检测,不过这个是后话了。也是通过这个项目,我们团队熟悉了在 hadoop 和 storm 环境下的开发,值得一提的是,通过使用 storm 我们把检测延时大大缩小了,另外由于把 storm 性能压榨太腻害,我们在一次事故中发现了 storm 的一个深层 bug,storm 中关于这个 bug 的修复代码就是我们提交的。作为一个土鳖,我们很自豪可以把 storm 玩到这个地步。

另外一个收获是,为了在应急响应时查询日志方便,我们把常用的日志部署在 ELK 集群上。起先没有经验,每天大约数十 T 的日志部署在常见的机械硬盘上,运行起来非常慢,一个查询内存居然还爆了。后来在大数据部的大拿指导下,我们混合使用了固态硬盘和机械硬盘,启用单机多示例,优化内存和 java 配置等,搭建起了 50 台物理服务器的 ES 集群,每台机器上双实例,当时 github 也才维护了不到二十台 es 服务器。同样在实战中我们熟悉了 kafka、hadoop 的优化,这个让我的 team 也有了大数据处理使用经验,这也为后面我们完全转向安全产品团队打下了基础。这种通过更底层进行降维对抗的思想,也影响了我的安全观,后面我们开源的 openrasp 也是这一思路的另外一种体现。

土鳖 PGM

机缘巧合,又遇到一次方向调整,部门的重点是对外提供商业安全产品,为此我们还收购了一家公司,这个是对我影响比较大的一次调整。相对于办公网和生产网的安全,商业安全产品的收益更加容易量化,而且可以服务更多的用户,得到更多的一线反馈。以前在游泳池游的,现在可以在大海里游了。

这时我们已经有 WAF 和抗 D 产品了,以及渗透测试服务。现在需要做的是丰富产品线满足不同层次的需要。起先我想到的是把 webdir 和 dbmon 产品化,因为确实效果不错。但是和几个用户聊完后,不是很感冒。先说 webdir 吧,在我们公司内部部署啥都好说,毕竟我们够强势去做这个事情,运维的同学不管心里服不服,表面上还是认可我们的。但是在不少互联网公司,安全工程师没有那么强势,恰巧在服务器上安装安全软件,容易导致一些纠缠不清的问题。所谓纠缠不清只可意会不可言传。

另外,程序需要直接扫描 web 代码文件,这个又是个敏感问题。dbmon 的问题也是类似,尤其是对于不少公司,数据库是不开启日志的,更别说是还要把日志从服务器搜集上来了。换句话来说,如果是影响业务的检测类产品,没准可以有市场。于是我们抱着尝试的心态,也没和老板吹啥牛,默默先做产品化,小步快走。我们把之前我们在公司内部做全流量镜像分析的系统做了产品化,相比于公司内部起步价 20G、50G 甚至几百 G 的带宽,用户侧上 1G 的都很少,于是我们做了很多简化处理,更多考虑的是便于部署和稳定性。

整个移植的过程其实比较简单,毕竟有点杀鸡用牛刀的感觉。销售侧也帮我们找到了几个天使用户,由于产品比较新,售前不懂,我就和销售去和用户介绍。还记得第一个用户部署测试的时候,第一天就发现了潜伏了好久的一个后门,当时正有人在使用这个后门。用户那安全设备其实部署的也不少,但是还是有这个后门,当时对方的安全负责人一激动就说他们全部机房都要部署这个。于是第一单就这么成了,互联网公司果然就是爽快。后面一段时间,我和销售一起见过不少客户,通常我们测试的用户都会有微信群,大家反馈问题都在微信群里,我那段时间经常在微信群里和用户沟通交流,产品侧的问题我们很快就迭代修改,经常上午反馈的问题我们下午就可以上线。

很多人好奇我回微信咋那么快,其实我对手机的重度使用也是那个时候开始的。我的好脾气也在这个时候展现了优势,很多产品细节使用的问题,每个好脾气就会忽略了。现在回顾那段时光,其实我们产品最后可以有不少用户,得力于从售前、测试、研发和售后的沟通顺畅,基本是把我搭进去了。我也使用过商业产品,通病是这四个环节非常脱节,越大的公司问题越大,因为公司越大,分工越细。售前不懂技术细节,满口跑火车的也不少见,售后只会抱怨说不清产品问题所在也正常。研发天天赶进度,自嗨的增加功能,不屑与没技术含量的功能修改也是常事。在我们产品的初期这些问题很少出现。一直到现在我都对这段经历很感慨,如果一个产品经理能说出自己产品哪里牛逼,其实不算牛逼,如果还能说出自己产品哪里不如竞争对手的,这个才算有点牛逼了。各种原因吧,其中肯定也包含我这段经历的加分,我后面负责了整个 web 安全产品的产品和技术,我们内部叫做 PGM,整体打通去看这些产品。有人说我安全圈认识人咋这么多,很多一部分是这个时间积累的,其实我在外面开会扯的很少。

兜哥带你学安全

不得不提的是我的公众号,这与我之前做安全产品的经历有一定的关系。我接触到不少从事互联网公司一线安全工作的童鞋。刚接触我的时候还是蛮防着我的,生怕我是来骗钱的。其实也可以理解,有点预算的甲方,估计都被乙方洗脑 N 遍了。另外我长的比较喜感,眼神比较清纯,人来熟,不像大家对黑客或者说安全从业人员的印象。接触几次后逐渐建立了信任,大家也比较聊的开了。虽然安全技术一直在演进,各种新的思想和概念也不断涌现,几乎每年安全会议的侧重点都会不太一样,大数据、AI 再到区块链。安全的攻击形式也层出不穷,针对 web 的、智能硬件的、AI 模型的等等。

但是一个现实是,甲方的需求和乙方介绍的技术和产品的鸿沟却一直不断扩大。一些很基础的安全加固知识可以搞定的,一些通过配置就可以搞定的事情,黑产关注了,但是大家却没怎么关注。mongodb、es 之类匿名访问放到公网裸奔,直到被加密勒索了;没打补丁的 window 服务器防火墙也不打开,还对外提供服务,直到被人全盘加密勒索;智能摄像头的 root 密码也不改,放到公网还拍些敏感内容,结果被人一个初始密码就劫持了。

我把我的一些经验和大家分享了下,发现大家蛮感兴趣。于是我抱着玩的心态,开始写我的公众号。起先我也没有多大把握会有多少人看,毕竟讲的都是些基础干货,比如如果做网络区域划分和隔离策略,如何对无线网络安全加固,生产网的服务器如何做加固等等。开始的时候,我是遇到用户问的问题,就把公众号里面相关的发给大家看。没想到一下子转发的很多,关注用户数涨的不错。最后我把文章进行了分类,分为了企业安全和 AI 安全两个板块。我公众号并不追求思路多新多领先,就是想成个小笔记本,大家有需要时可以找到使用的办法,一不小心成了一个粉丝数相当不错的安全自媒体。

AI 探索

大概在 2014 年底还是 15 年初,组织架构调整,我收了一个研究机器学习的小团队。团队的 leader 是个非常活跃的小孩,而且特别有激情。他一直鼓捣我增加在机器学习方面的投入。

当时 HC 控制非常严格,但是我还是被他传销式的汇报感动或者说屈服,我尽力支持他。当时我们在线上环境的离线数据上做了大量的尝试,在部分场景下也取得了不少进展。也是这个阶段,我对 AI 建立起了信心。我开始重新梳理 AI 比较成功的使用场景,然后尝试移植到安全领域。那段时间我自学了机器学习,也经历过激情澎湃地买了一堆机器学习书籍后发现一个公式也看不懂的尴尬,几次想放弃。最后我发现自己是码农出身,我对代码的理解能力远强于文字和公式,于是我从有示例代码的书籍学习起,逐渐理解了机器学习的常见算法。

如何挖掘场景并使用合适的算法呢?

这个确实靠悟性和经验,很难就靠看书就理解了,需要大量的实践。我投入了大量的个人时间用于学习和实践。熟悉我的人都知道我喜欢看电子书,我的电子书里除了老罗的书和几本历史的书,基本都是机器学习的书。每天上下班有将近一个小时在城铁上看书,算起来一个月就是 20 个小时,约 3 个工作日。回顾这段学习的经历,我的感受是机器学习的学习坡度很陡,所以很多人会半途放弃或者一知半解,但是这恰恰是它的门槛。 sqlmap 的常见命令一天掌握问题不大,你觉得是门槛吗?

写书

为啥会写书呢?

说起来原因很简单,因为我的公众号和文章经常被人抄,有去掉我水印的,有去掉我图片的,还有完全抄只是改作者名称的。抄袭的有自媒体,有小媒体,还有一些厂商,我也无力吐槽和维权。最后我就写书吧,抄我的好歹你要买一本复印。也正是这段被抄袭的经历,我的书和 PPT,尽量连引用的图片也标注,算是一种尊重吧。

写书的选材,我选择了 AI 安全,而不是企业安全。因为实话实说,企业安全也不急于一时,市场上已经有了,但是 AI 安全的却没有。另外,大家其实对于如何使用 AI 做安全更多停留在概念层面。更有甚者,在 PR 稿上就是罗列一堆公式,然后说人能识别威胁,人能看病,所以他能用 AI 搞定。我也只能呵呵。

基于这复杂的原因,我开始写我的 AI 安全书籍。由于 AI 的知识太多,最终定稿成三本,一本讲入门的,叫《web安全之机器学习入门》,一本讲实战的,叫《web安全之深度学习实战》,目前都已经出版了。出版前我很担心卖不出去,结果让我非常意外,从甲方到乙方,从国内到国外,都有我的读者,就在今天美国 fireeye 的一位总监,当然也是我好友还在朋友圈 show 我的书。还要感谢我的几位老板帮我写序,以及众多业内好友的帮助。据说我的书还入选了出版社的计算机类年度十佳。感谢 Freebuf 的提名,我在 FIT 年度安全人物评选排到第三。

生命不息折腾不止

也许我继续做我的安全产品,今天改个 bug,明天加个按钮,日子也慢慢也会过去。具体的产品也有具体负责的经理,我把经理管好就 ok 了,我也可以过的比较 happy,就和以前巨牛无比的万能充一样。当年万能充卖的火热时,哪里会想到现在充电接口统一后,再难见到它的踪影。

时代抛弃你的时候,连招呼都不会打。尤其是你觉得舒适的时候。

我自己研究 AI,我的几次转型,其实都是我主动的走出了舒适区。就像我最近,以 30 岁高龄,从带团队的,转去实验室稿新产品预研一样。在一堆 20 多岁的小伙子高呼搞技术没用,要搞管理岗的时候,我选择了在一个技术型公司继续深耕技术。我有自己的技术理想,我觉得搞这些蛮开心不枯燥。

精彩待续

我的奋斗还在继续,精彩等着我继续谱写。

2008 年,我是看着《我的华为十年》这篇文章进入这家公司的,当时我的总监就是这篇文章的作者家骏。转眼云烟,第一份工作做到了现在。

菜鸟入职

我入职的时候,公司规模远没有现在这么大,北京地区的研发零星分散在中关村的几个写字楼,包括理想国际、普天大厦和银科大厦。

我是在普天大厦入职的,记得当时是 12 月份,第一次冬天来北京的我,领会到了啥叫冰雪两重天。

在武汉是没有暖气的,屋外四度,屋里也是四度。北京是有暖气的,屋外零下十度,屋里起步价二十度。

公司里不少大姐大哥在公司里面穿的和夏天一样,出门套个大棉袄正合适。

我没经验,带着一身毛衣毛裤来的,结果在外面还是冻死,在公司里热死。

我大二的时候开始给华为三康做一些项目,在公司里面是不能上外网,而且都是又重又大的台式机。

入职的时候,行政小哥发给我个笔记本,我当时一激动就问了一句,这个回家加班可以用不?

行政小哥一脸懵逼,为啥不可以呀。

刚到北京的第一个月我是住在南二环的亲戚家,公司在拥挤的宇宙中心中关村,每天基本我就跟取经一样,天蒙蒙灰就出门,天漆漆黑回家。

第一个任务:防火墙双机上线

每一个自己觉得自己很牛逼的公司,都会有一个精心设计的新员工培训,有的像传销,有的像传教,有的讲情怀,有的忆苦思甜。我对新员工培训唯一的印象就是别在公司抽烟和养宠物,其他随便。可见当初一定有个在公司抽烟又养宠物的人把大佬惹毛了。虽然我不抽烟,但是我想养个小乌龟小金鱼,也只能放弃。

我的第一个任务是做防火墙双机上线,首先感谢组织信任,其次觉得专业好像有点不对口,我是个 rd 呀,虽然我懂点网络,不过也是写过交换机的软件而已。于是我硬着头皮看白皮书,看配置命令,幸好一起升级的还有经理黄姐和一个老员工永校,感觉自己打下手应该问题也不大。第一个任务总不能搞砸嘛,我还蛮认真的画了网络拓扑和配置回滚方案。其实仔细想想,双机以前不就是一个防火墙吗,现在就是再放一台上去呗。

为了不影响业务,我们选择在元旦凌晨上线。上线的过程确实非常顺利,简单描述,就是把新防火墙放上机架,插上电,配置灌进去,接线,搭完收工。十分钟不到搞定了,我都准备撤了。

老员工说,这才哪到哪呀,我们要验证可以自动切换。新防火墙和老防火墙之间有两根心跳线,汇聚层有大概 6 台汇聚层交换机,分别会连到两台防火墙上,要验证诸如心跳线段,上联线段的情况。另外国内众所周知的原因,分为南北网,联通电信互通很差劲,防火墙上联四根运营商的链路,还要测试这几根线路断的情况下的自动切换。于是乎,不停插拔网线,ping 新浪 ping 搜狐。测试完凌晨 4 点了,总算搞得差不多了。我记得我走出普天大厦的时候,居然看到了 2009 年的第一个日出了。

第一个项目:准入系统 BNAC

我的第一个项目是开发准入系统。所谓的准入系统,简单讲,就是上网认证,主机安全检查加上网络权限控制。满足一定安全基线要求的终端才允许接入公司办公网,并且根据不同的部门和职位,赋予不同的网络访问权限。

公司当时已经买了号称全球顶尖的准入系统,不过在易用性和可定制性上差强人意。另外一个原因,我们总监最初在华为就做了第一套准入系统,他对准入的理解非常深刻,从他的角度来看,目前这个国外的准入系统,有线无线不能自动切换,不能和微软的域管理集成,权限管理过于死板,最坑爹是对网络设备有要求,捆绑销售他们的防火墙。

这些在传统企业不是太大问题,但是在互联网公司就是硬伤了。于是我入职前基本就拍下来要自己研发准入系统。他老人家是理解深刻,我理解不深刻呀,从网上搜了个遍,还是一知半解。还好有个老安全工程师志刚领路,介绍了一些厂商进行交流,总算整明白咋回事了。于是开工干活,整个系统分为客户端,策略管理平台,测试管理服务器和防火墙。防火墙由系统部的一个大拿负责写网络控制模块,现在这哥们是我们公司 CDN、流量清洗这些基础设施的负责人。

客户端的主机检查模块由我们另外一个安全工程师负责,他现在也是 BAT 某公司的高 P 了。其他都是我弄的,第一次写网页还有点小兴奋,尤其是自己画 logo,尽显人文修养。我在学校用 delphi 写了系的教师考评系统,在那个年代 dephi + sqlserver 是绝配,access 也面对小 case 也是 ok 的,因此我们考核系统也是 cs 架构的。惯性思维,我的客户端也是用 delphi 写的。为了支持使用 linux 办公的同学,还开发了 linux 客户端。

网页完全是新接触,用了当时比较新的 groovy。这个时期我接触了大量新知识,这些开发语言还是其次,主要是认识了不少网络设备,接入层的从啥 hub 到二层交换机,三层交换机,还有啥无线 AP 和 AC。和部署实施比起来,开发这个阶段是多么美好的回忆,事实上我差不多 3 个月开发完了第一版,为了验证有效性,我们打算在部分办公区部署。于是我们开始杀熟,先在我们部门使用,我待人和气的好脾气也是这个阶段养成的。

我们把我们部门的办公区的接入交换机和汇聚层交换机之间传入了防火墙。所谓的防火墙其实就是台服务器,最早用的是 dell 的 2850。现在看起来 2850 的配置确实差的可怜,四核八G内存,六块七十三 G 的大硬盘,还齁沉齁沉的。我一个人搬它还很费劲,经常要和一个叫大肉的老员工一起搬。

由于当时交换机的机柜就在办公区,我们的防火墙也只能放办公区。别看 2850 配置不行,风扇确极其彪悍,一开机地动山摇,半层楼能听见。经常可以听到旁边部门骂,谁这么缺德把服务器放办公区,还让不让人上班生孩子啥的。

我们公司没有花名这一说,但是我处于怕人知道我真名骂我,我很早就用花名了。差不多那个时候开始叫麦兜了,至少名字这么可爱,大家骂的时候也有所估计,后来岁数大了开始叫兜哥了,这也是我网名的由来。

总被骂,确实也觉得对不起大家,所以一直到现在也待人客气。还好除了吵,基本没出现过断网的问题,偶尔出现过奇葩软件和客户端不兼容的情况,也很快解决了。在那个蠕虫病毒泛滥的年代,我们通过准入系统强制电脑安装杀毒、安装补丁、开启防火墙等等,简直就是功德无量,很多年都没有发生过大面积的病毒感染,一直服役到现在,差不多有 9 年了。我到现在还记得家俊在部门会上说某某厂做准入几百人,我们就搭进去个麦兜。

枪版网工生活

2009 年,全部门的重点就是建设新大厦,所谓的新大厦,就是现在我们叫的老大厦,就是西二旗旁边那个百度大厦。当时网络工程师就三个人,大肉,秀英和永校,人手不够就把我也搭上了。我是革命一块砖,哪里需要新员工哪里搬。

这次真成网络工程师了。小时候觉得工程师很牛逼,工作后发现其实叫网工更合适,就和电工一样。好在安全工程师说起来还有点黑客帝国的感觉,即使简称安工,也感觉和同仁堂的救命神药安宫硫磺一样牛逼哄哄的。不过我们这几位网工可牛逼了,一个是 3com 和华为研发出身的,另外一个是 2008 奥运会的网络建设负责人之一,相比我就是渣渣了,而且还是业余渣渣。

给我的第一个任务是生成全部网络设备的配置,大概是 500 多台有线交换机,200 多台无线 ap 和交换机的配置。当时在奥运会的时候,他们是规划好 IP 地址后,通过一个 java 的程序手工配置参数后生成一个设备的配置,然后通过 securecrt + js 脚本 + 串口把配置文件灌入设备。有多少台设备就要手工配置多少次,不过这个已经比手工写配置文件牛逼很多了。

当时对我的预期是把思科设备的文件改成华为设备的。我对网络设备的配置完全是工作后学的,半桶水都不到,也是这个时期我学会了思科和华为设备的使用和配置方法。我看懂了原来的程序后,发现其实把网络规划体现到电子表格后,通过程序读取数据,可以一次性生成全部配置。而且这都是纯文本的活,用 perl 更合适。于是我重头开始写,差不多一周多业余时间完成了 demo,当时我还写准入在。

中间也出过不少问题,比如关键字写错了,思科的一些命令没改,有些参数写死了,最后又改了几次才能用,虽然被骂的也挺惨,不过最后对我评价是超出预期,大大节省了网工的工作量,唰唰唰 10 分钟可以生成全部配置。尤其是有次网工发现自己电子表格写错了,要是以前他需要一台一台配置去生成,但是我这边重新 run 一下就好了。

那段时间我还在陪大肉升级交换机 OS 和灌配置的过程中自学了 CCNA 和 CCNP,现在也很怀念和大肉在信威大厦里面灌配置的日子。那段时间玩命加班,几乎 3 个月没有咋过双休,最后竣工的时候居然有了 19 天调休。一直到现在,如果面有网路经验的安全工程师时,我总能扯好久,一直可以问傻别人,也是这段时间积累的。

内部安全建设黄金时代

这个时期是我们公司内部安全建设的黄金时代,很大一个原因是我们有了级别非常高的 CIO John Gu。John 长期在国外工作,一直做到几个巨型企业的 CIO。相对国内私企,国外企业对安全重视很多。和 John 不用太多介绍安全的重要性,而是想好怎样做好安全就可以了。为了有更好的视野,我们还挖来了埃森哲的架构师欧阳。大概有 2-3 年的时间,我们都有非常充足的预算的进行安全建设,我也开始带 team。这段时间我的工作才开始接近我理解的安全工程师和甲方安全。

这段时间我比较系统的建设了内部安全体系,从企业杀毒、终端补丁管理、DLP、邮件安全网关、IPS、漏洞扫描器、上网行为审计、APT 检测到终端安全加固、软 token、堡垒机、应用虚拟化、硬盘加密、文件加密等。那个时期,负责互联网公司的国外安全厂商的销售,应该大部分认识我。这段时间,是我安全知识面扩展非常快的一个时期。一直到现在,我跟许多解决方案架构师沟通很顺畅,也是得益于这个时期积累的知识。我后面可以承担 PGM 的工作,有相当一部分原因是我对安全产品需求的感觉,这种感觉的培养其实也来自于我这段经历。

云安全部成立

在很长一段时间,我们没有安全部,安全的职责分散在技术体系下不同部门的几个组里面。早期问题并不大,大家各司其职,但是当公司发展到一定程度后,对外的产品线日趋繁杂,内部的协同配合压力日趋变大。于是在某年某月的某一天,我们几个分散的小组合并成立一个新部门,曰云安全部。人员合并后按照每个人的技能重组团队,我负责基础架构安全的 team,曰 isec,职责范围包括内部网和生产网。我的核心 team 成员也是从那个时候一直和我到现在,现在想想也真不容易。

与内网相比,生产网有趣很多,安全工程师的压力也大很多。物理服务器的数量达到数万甚至数十万,虚拟机以及容器数量起步价也是百万级了,出口带宽几百 G 的机房遍布全球,涉及的产品线更是复杂到令人发指,只要想的到的业务几乎都有,想不到的没准也有。相对内部网,生产网攻击面大很多,毕竟这些业务是 7 乘 24 对数亿网名提供服务的。我们面对的最大挑战就是如何在业务不中断,不损失访问流量的情况下保障业务的安全。因此我们的重点一个是安全加固,一个是入侵检测,其中入侵检测是我很喜欢的一个领域。在国内,入侵检测经常被理解为 IDS/IPS 这样的安全设备。在以 web 浏览访问以及手机 app 访问为主要业务形式的互联网公司,入侵检测覆盖分范围非常广泛。

我首先遇到的一个问题其实不是技术上的,如何衡量我们所做的努力对公司安全状况的贡献。换句话来说,就是如何描述我们做的事的产出。在大多数公司,甲方安全都是地道的成本中心,纯成本消耗。如何证明安全团队的价值是非常重要的,即使是在一个超大型互联网公司。我观察到有些同学其实干的也很苦逼,情绪低落,总是抱怨。确实他负责很多小项目,每个事情看似很重要,但是确实也看不到啥产出,感觉做不做其实也一样。于是明显的恶性循环也产生了,一个事情没做成业绩,就继续做另外一个,结果下一个也没做出成绩,继续做下一个,手上一堆烂尾楼,还要抱怨辛苦没人看到。

在那个时候,某知名漏洞平台还在,上面报的漏洞公司层面还是非常重视的。于是我想到一个重要的衡量指标,就是安全事故的主动发现比例。比如拿到服务器的 webshell,SQL 注入点和敏感文件下载,这些都是影响大且容易量化的。如果能够通过我们开发的入侵检测系统,提高我们主动发现入侵事件的比例,这个贡献是非常容易体现的。我们在相当长的一段时间就是从各个维度想办法提高这些指标,其中印象深刻的就是 webdir 和 dbmon。

webdir&dbmon

webdir 和 dbmon 是我们内部取的名字,简单讲 webdir 分析 web 服务器上的文件,及时发现后门文件,dbmon 分析数据库日志,及时发现 SQL 注入点以及拖库行为。通过这两个项目,我的 team 从一个安全技术团队开始向一个安全产品团队衍变,除了负责应急响应和渗透测试的的安全工程师,开始出现有安全背景的研发工程师以及负责 storm 和 hive 的大数据工程师,人数也开始两位数了。

webdir 在一期的时候,主要是依赖收集的样本提炼的文本规则,简单有效,在部署的初期发现了不少 case,部署的范围主要集中在重点产品线,量级在一万台左右。我们在二期的时候,重点工作是一方面提高检测能力,一方面是减少发现的延时,另外一个方面是全公司部署,这三方面都是为了提高 webshell 的主动发现比例。

在检测能力方面,主要是提高准确率和召回率,关于这两个指标的含义,有兴趣的同学可以看下我机器学习的书,里面用小龙虾和鱼来做了形象的比喻。基于文本特征的 webshell 检测,很难在这两个指标之间做平衡,尤其是我们这种超大规模的公司,即使是每天新增的文件也可能上亿,实验室环境看着还蛮不错的检测效果,误报也会被放大。因此大多数安全工程师的选择就是写极其精准的规则,所谓精准,就是根据搜集的样本写的过于严格苛刻的规则,用于大大降低误报。这导致的结果是,误报确实少了,但是漏报也非常严重。

我们仔细研究了下问题所在,主要是由于 php 语言的高度灵活性,一个很简单的功能可以用多种方式实现,还有不少装逼的语法。单纯在语言文本特征层面做非常吃力。通过调研,我们发现不管文本特征层面如何做绕过我们的检测,最后 webshell 还是要以 php 和 java 的语法来实现,如果我们可以实现 php 和 java 的语法,就可以在更底层提取特征,与黑产进行对抗。

这个思路也一直影响了我们后面的流量分析产品以及基于机器学习的 webshell 识别,不过这个是后话了。这个思路也成为我们二期的主要提升点,当时根据我们搜集的数千样本,挑选了专业的安全产品进行测试对比,我们的两个指标综合领先。我们另外的一个挑战是工程上的,我们仅在国内就有大量的机房,每个机房之间的带宽不尽相同,而且使用率也大不相同,即使是固定的两个机房,带宽使用也有明显的时间特征。

另外互联网公司大多把服务器的性能压榨的非常腻害,运维部门对我们的性能指标限制的非常死,甚至超过一定的 CPU 或者内存就会自动把我们进程挂起甚至 kill。为了尽可能降低服务器的性能消耗,我们使用云模式,负责的语法解析与规则匹配放到云端,服务器上仅需要完成非常简单的处理和上传逻辑。但是几十万个服务器如果因为上线新版本同时出现新文件需要检测,也可能会出现带宽的异常消耗,于是我们也使用了去中心化的部署方式。

一群只玩过单机版 syslog-ng 分析日志的土鳖,一下子可以有上百台服务器,还用上了大型消息队列和自研的沙箱集群,想想确实很有成就感。二期上线后,无论从部署范围还是检测能力上,都上了一个新台阶,并且由于检测技术上的创新以及客观的评测结果,这个项目获得了公司层面的创新奖。在这个项目上另外一个收获是开发服务器端的程序的经验,在一个如此大规模的集群上部署客户端,还要做到性能消耗小,考虑各种异常情况的处理,考虑各种兼容性问题,这些都是干过才能积累的。

dbmon 在一期的时候,依托于公司运维部的 DBA 团队的现有系统,离线分析公司部门产品线托管的 mysql 查询日志。检测的效果确实不理想,一堆暴力破解的报警,仔细一查都是密码过期了。检测的重点没有放到 SQL 这些上,而是更像针对数据库的异常访问检测了,这个其实从实践角度,安全人员很难去定位问题,小同学弄两次就烦了,所以效果一直很差,最后运维系统的同学根本不想看报警了。

二期的时候我们聚焦到 SQL 检测上,相对于 waf 和流量层面,SQL 日志层面做 SQL 注入点检测非常合适,因为在 http 协议层面可以有大量绕过 sql 注入检测的技巧,但是最终还是会落地到可以执行的 SQL 语句,在 SQL 日志层面会大大简化这方面的检测,相对于负责的 WAF 规则,SQL 日志层面上的检测是在黑客难以控制的更底层进行对抗。在这个阶段即使是文本特征的检测,在准确率和召回率上表现已经不错了。

上线效果非常好,同学们对这个也有了信心。集思广益,在三期的时候我们在 SQL 层面尝试了也使用语法而不仅仅是文本规则检测,不过这个是后话了。也是通过这个项目,我们团队熟悉了在 hadoop 和 storm 环境下的开发,值得一提的是,通过使用 storm 我们把检测延时大大缩小了,另外由于把 storm 性能压榨太腻害,我们在一次事故中发现了 storm 的一个深层 bug,storm 中关于这个 bug 的修复代码就是我们提交的。作为一个土鳖,我们很自豪可以把 storm 玩到这个地步。

另外一个收获是,为了在应急响应时查询日志方便,我们把常用的日志部署在 ELK 集群上。起先没有经验,每天大约数十 T 的日志部署在常见的机械硬盘上,运行起来非常慢,一个查询内存居然还爆了。后来在大数据部的大拿指导下,我们混合使用了固态硬盘和机械硬盘,启用单机多示例,优化内存和 java 配置等,搭建起了 50 台物理服务器的 ES 集群,每台机器上双实例,当时 github 也才维护了不到二十台 es 服务器。同样在实战中我们熟悉了 kafka、hadoop 的优化,这个让我的 team 也有了大数据处理使用经验,这也为后面我们完全转向安全产品团队打下了基础。这种通过更底层进行降维对抗的思想,也影响了我的安全观,后面我们开源的 openrasp 也是这一思路的另外一种体现。

土鳖 PGM

机缘巧合,又遇到一次方向调整,部门的重点是对外提供商业安全产品,为此我们还收购了一家公司,这个是对我影响比较大的一次调整。相对于办公网和生产网的安全,商业安全产品的收益更加容易量化,而且可以服务更多的用户,得到更多的一线反馈。以前在游泳池游的,现在可以在大海里游了。

这时我们已经有 WAF 和抗 D 产品了,以及渗透测试服务。现在需要做的是丰富产品线满足不同层次的需要。起先我想到的是把 webdir 和 dbmon 产品化,因为确实效果不错。但是和几个用户聊完后,不是很感冒。先说 webdir 吧,在我们公司内部部署啥都好说,毕竟我们够强势去做这个事情,运维的同学不管心里服不服,表面上还是认可我们的。但是在不少互联网公司,安全工程师没有那么强势,恰巧在服务器上安装安全软件,容易导致一些纠缠不清的问题。所谓纠缠不清只可意会不可言传。

另外,程序需要直接扫描 web 代码文件,这个又是个敏感问题。dbmon 的问题也是类似,尤其是对于不少公司,数据库是不开启日志的,更别说是还要把日志从服务器搜集上来了。换句话来说,如果是影响业务的检测类产品,没准可以有市场。于是我们抱着尝试的心态,也没和老板吹啥牛,默默先做产品化,小步快走。我们把之前我们在公司内部做全流量镜像分析的系统做了产品化,相比于公司内部起步价 20G、50G 甚至几百 G 的带宽,用户侧上 1G 的都很少,于是我们做了很多简化处理,更多考虑的是便于部署和稳定性。

整个移植的过程其实比较简单,毕竟有点杀鸡用牛刀的感觉。销售侧也帮我们找到了几个天使用户,由于产品比较新,售前不懂,我就和销售去和用户介绍。还记得第一个用户部署测试的时候,第一天就发现了潜伏了好久的一个后门,当时正有人在使用这个后门。用户那安全设备其实部署的也不少,但是还是有这个后门,当时对方的安全负责人一激动就说他们全部机房都要部署这个。于是第一单就这么成了,互联网公司果然就是爽快。后面一段时间,我和销售一起见过不少客户,通常我们测试的用户都会有微信群,大家反馈问题都在微信群里,我那段时间经常在微信群里和用户沟通交流,产品侧的问题我们很快就迭代修改,经常上午反馈的问题我们下午就可以上线。

很多人好奇我回微信咋那么快,其实我对手机的重度使用也是那个时候开始的。我的好脾气也在这个时候展现了优势,很多产品细节使用的问题,每个好脾气就会忽略了。现在回顾那段时光,其实我们产品最后可以有不少用户,得力于从售前、测试、研发和售后的沟通顺畅,基本是把我搭进去了。我也使用过商业产品,通病是这四个环节非常脱节,越大的公司问题越大,因为公司越大,分工越细。售前不懂技术细节,满口跑火车的也不少见,售后只会抱怨说不清产品问题所在也正常。研发天天赶进度,自嗨的增加功能,不屑与没技术含量的功能修改也是常事。在我们产品的初期这些问题很少出现。一直到现在我都对这段经历很感慨,如果一个产品经理能说出自己产品哪里牛逼,其实不算牛逼,如果还能说出自己产品哪里不如竞争对手的,这个才算有点牛逼了。各种原因吧,其中肯定也包含我这段经历的加分,我后面负责了整个 web 安全产品的产品和技术,我们内部叫做 PGM,整体打通去看这些产品。有人说我安全圈认识人咋这么多,很多一部分是这个时间积累的,其实我在外面开会扯的很少。

兜哥带你学安全

不得不提的是我的公众号,这与我之前做安全产品的经历有一定的关系。我接触到不少从事互联网公司一线安全工作的童鞋。刚接触我的时候还是蛮防着我的,生怕我是来骗钱的。其实也可以理解,有点预算的甲方,估计都被乙方洗脑 N 遍了。另外我长的比较喜感,眼神比较清纯,人来熟,不像大家对黑客或者说安全从业人员的印象。接触几次后逐渐建立了信任,大家也比较聊的开了。虽然安全技术一直在演进,各种新的思想和概念也不断涌现,几乎每年安全会议的侧重点都会不太一样,大数据、AI 再到区块链。安全的攻击形式也层出不穷,针对 web 的、智能硬件的、AI 模型的等等。

但是一个现实是,甲方的需求和乙方介绍的技术和产品的鸿沟却一直不断扩大。一些很基础的安全加固知识可以搞定的,一些通过配置就可以搞定的事情,黑产关注了,但是大家却没怎么关注。mongodb、es 之类匿名访问放到公网裸奔,直到被加密勒索了;没打补丁的 window 服务器防火墙也不打开,还对外提供服务,直到被人全盘加密勒索;智能摄像头的 root 密码也不改,放到公网还拍些敏感内容,结果被人一个初始密码就劫持了。

我把我的一些经验和大家分享了下,发现大家蛮感兴趣。于是我抱着玩的心态,开始写我的公众号。起先我也没有多大把握会有多少人看,毕竟讲的都是些基础干货,比如如果做网络区域划分和隔离策略,如何对无线网络安全加固,生产网的服务器如何做加固等等。开始的时候,我是遇到用户问的问题,就把公众号里面相关的发给大家看。没想到一下子转发的很多,关注用户数涨的不错。最后我把文章进行了分类,分为了企业安全和 AI 安全两个板块。我公众号并不追求思路多新多领先,就是想成个小笔记本,大家有需要时可以找到使用的办法,一不小心成了一个粉丝数相当不错的安全自媒体。

AI 探索

大概在 2014 年底还是 15 年初,组织架构调整,我收了一个研究机器学习的小团队。团队的 leader 是个非常活跃的小孩,而且特别有激情。他一直鼓捣我增加在机器学习方面的投入。

当时 HC 控制非常严格,但是我还是被他传销式的汇报感动或者说屈服,我尽力支持他。当时我们在线上环境的离线数据上做了大量的尝试,在部分场景下也取得了不少进展。也是这个阶段,我对 AI 建立起了信心。我开始重新梳理 AI 比较成功的使用场景,然后尝试移植到安全领域。那段时间我自学了机器学习,也经历过激情澎湃地买了一堆机器学习书籍后发现一个公式也看不懂的尴尬,几次想放弃。最后我发现自己是码农出身,我对代码的理解能力远强于文字和公式,于是我从有示例代码的书籍学习起,逐渐理解了机器学习的常见算法。

如何挖掘场景并使用合适的算法呢?

这个确实靠悟性和经验,很难就靠看书就理解了,需要大量的实践。我投入了大量的个人时间用于学习和实践。熟悉我的人都知道我喜欢看电子书,我的电子书里除了老罗的书和几本历史的书,基本都是机器学习的书。每天上下班有将近一个小时在城铁上看书,算起来一个月就是 20 个小时,约 3 个工作日。回顾这段学习的经历,我的感受是机器学习的学习坡度很陡,所以很多人会半途放弃或者一知半解,但是这恰恰是它的门槛。 sqlmap 的常见命令一天掌握问题不大,你觉得是门槛吗?

写书

为啥会写书呢?

说起来原因很简单,因为我的公众号和文章经常被人抄,有去掉我水印的,有去掉我图片的,还有完全抄只是改作者名称的。抄袭的有自媒体,有小媒体,还有一些厂商,我也无力吐槽和维权。最后我就写书吧,抄我的好歹你要买一本复印。也正是这段被抄袭的经历,我的书和 PPT,尽量连引用的图片也标注,算是一种尊重吧。

写书的选材,我选择了 AI 安全,而不是企业安全。因为实话实说,企业安全也不急于一时,市场上已经有了,但是 AI 安全的却没有。另外,大家其实对于如何使用 AI 做安全更多停留在概念层面。更有甚者,在 PR 稿上就是罗列一堆公式,然后说人能识别威胁,人能看病,所以他能用 AI 搞定。我也只能呵呵。

基于这复杂的原因,我开始写我的 AI 安全书籍。由于 AI 的知识太多,最终定稿成三本,一本讲入门的,叫《web安全之机器学习入门》,一本讲实战的,叫《web安全之深度学习实战》,目前都已经出版了。出版前我很担心卖不出去,结果让我非常意外,从甲方到乙方,从国内到国外,都有我的读者,就在今天美国 fireeye 的一位总监,当然也是我好友还在朋友圈 show 我的书。还要感谢我的几位老板帮我写序,以及众多业内好友的帮助。据说我的书还入选了出版社的计算机类年度十佳。感谢 Freebuf 的提名,我在 FIT 年度安全人物评选排到第三。

生命不息折腾不止

也许我继续做我的安全产品,今天改个 bug,明天加个按钮,日子也慢慢也会过去。具体的产品也有具体负责的经理,我把经理管好就 ok 了,我也可以过的比较 happy,就和以前巨牛无比的万能充一样。当年万能充卖的火热时,哪里会想到现在充电接口统一后,再难见到它的踪影。

时代抛弃你的时候,连招呼都不会打。尤其是你觉得舒适的时候。

我自己研究 AI,我的几次转型,其实都是我主动的走出了舒适区。就像我最近,以 30 岁高龄,从带团队的,转去实验室稿新产品预研一样。在一堆 20 多岁的小伙子高呼搞技术没用,要搞管理岗的时候,我选择了在一个技术型公司继续深耕技术。我有自己的技术理想,我觉得搞这些蛮开心不枯燥。

精彩待续

我的奋斗还在继续,精彩等着我继续谱写。

*本文作者:兜哥,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

最近思考人生比较多,对行业的发展也有些自己的思考。

1.jpg

业务安全

在大多数互联网公司,安全岗位都是属于成本部门。不考虑安全部门ROI的凤毛麟角。即使是超大型的几个头部公司,安全部门的汇报级别普遍较高,但是安全部门的工作重点都是不断证明自身的价值。对于绝大多数业务部门,基本是无法理解什么是XSS和SQL注入。

和业务部门解释这些,好比和一群素食爱好者介绍你们新推出的鸡腿堡套餐,不是说不说的清楚的问题,是傻不傻的问题。

唯一的例外,就是业务持续性业务安全这两个是可以和业务部门沟通的。前者最常见的就是被D了,业务中断了,一小时损失多少PV,多少流水,业务部门比安全部门算的清。但是前者安全部门可以做的很有限,大多数情况下还是要以来安全厂商来解决。后者除了依赖安全厂商,安全部门也可以有发挥余地。常见的业务安全问题多与黑灰产对抗有关系,比如虚假点击、虚假注册,恶意爬虫之类。这些业务安全问题,业务部门是比较容易理解的,安全部门的价值也是容易体现的。2018年,甲方安全的同学如果觉得自己做的事情价值业务方不理解,不认同,不妨尝试下业务安全。

物联网安全

物联网安全一直是一个大家觉得会火,事实上也比较火,但是貌似大多数厂商没赚到钱的领域。

早期针对物联网设备的攻击,主要驱动力是用于垃圾邮件僵尸网络,说的更直白一点就是通过垃圾邮件进行广告营销变现,在那个物联网设备计算和带宽资源都相当有限的年代,干这个事是不错的选择。

这几年情况发生了很大变化。

  1. 一方面物联网设备井喷式发展,数量极其惊人;

  2. 一方面物联网设备的计算和带宽资源更加丰富;

  3. 另外一方面,智能家居等有能力接触到大量个人隐私的物联网设备大量出现。

所以目前这对物联网设备的驱动力主要为:

  1. 组成DDoS僵尸网络,发起超大规模网络攻击;

  2. 挖矿,挖各种币。

  3. 窃取个人隐私数据,从图像、语音到健康数据、消费数据以及账户数据等。

物联网安全技术的发展不是最令人担心的,因为目前大多数设备在设计的最初没有考虑安全问题。基础的操作系统漏洞、以及协议层的重放攻击,应用层的弱密码都可以干掉一大批设备。一个根上的问题,是谁该为安全问题买单。最终受害的是消费者,但是指望消费者去解决安全问题吗?买个家用的物联网IPS?指望厂商也不太靠谱,目前物联网或者说智能家居,智能设备行业还属于野蛮发展阶段,除了极其有限的几个头部厂商有能力去解决自己的安全问题,数量极其惊人的发展中厂商主要精力还在解决活下来的问题,指望他们去主动解决安全问题也不靠谱。

整个行业范围的去解决这些安全问题,我个人理解比较靠谱的方式,是类似大型公有云平台解决客户安全问题的方式,就是在物联网的操作平台层去统一解决大部分问题。设备厂商只要使用统一的几种OS或者开发套件就可以解决大部分安全问题。2018年这方面大家拭目以待。

AI安全

这个领域是我觉得比较奇葩的领域。或许我对AI安全的理解太狭义了。我个人理解的AI安全主要是使用AI技术去赋能安全产品,比如让WAF和IPS更牛逼,我写了两本介绍AI安全的书《web安全之机器学习入门》和《web安全之深度学习实战》也主要是从这个角度介绍的。

但是目前市场上相当一部分甲方和乙方是把智能设备的安全也称为AI安全,当然这也没啥问题。

AI一直是个不温不火的领域,让整个世界重视AI,阿尔法狗功不可没。阿尔法狗展现了,AI可以在部分领域超越人类。推而广之,计算机的强大算力和并发处理能力,可以让AI最终可以以更低的成本更好的完成人类的一些工作。这确实非常有吸引力。

AI技术目前在语音和图像识别领域非常成熟,几个头部的AI公司也是围绕着两个技术。基于这两项技术的AI安全公司势必也可以给人以惊喜,比如智能安防,智能认证等。

另外在AI赋能安全产品领域,我个人理解在更加细分的领域,漏洞发现类的产品发展会快于防护类和监控类产品。因为AI尤其是深度学习,最大的一个特点就是不可解释性,虽然最新的发展已经可以部分解决卷积处理的结果,但是对于MLP、RNN这些还是难以以人类可以理解的逻辑去解释。这对于防护类和监控类产品非常尴尬,你拦截一个包,判断一个交易申请是欺诈,你还没法和人解释,这就尴尬了。但是对于扫描类,或者说智能渗透,这个问题就没有那么尴尬了。另外扫描类产品对于误报的容忍性比其他产品要强,偶尔误报也可以忍了。但是阻断类产品就没那么轻松了。我个人理解扫描类AI产品值得期待。

隐私保护

从几个头部公司到众多独角兽公司,从互联网公司到传统企业和政府,都接触到大量的个人隐私数据。基于大量客户数据二次挖掘,提升用户体验甚至直接产生经济价值,已经成为很多公司的选择。但是一旦这些隐私数据没有妥善的保存、处理以及操作审计,出现信息泄露事件后果都是很严重的。这两年基于个人隐私数据的PR战只是冰山一角。基于个人隐私数据的犯罪以及司法刑事案件已经层出不穷。典型的就是基于隐私数据的互联网欺诈。立法层面对企业搜集、保存和处理个人隐私数据进行约束已经逐步进行。尤其是国外,公司层面的个人隐私泄露将遭受巨额罚款。国内这方面还在起步,不过趋势肯定也是向国外看起。与个人隐私相关的大数据软件安全、SGX、TEE、差分隐私、同态搜索等技术将普遍使用,有兴趣的可以关注这块。隐私保护已经从制度和审计层面,走向了攻防对抗阶段。

区块链

区块链是比特币的底层技术。大量的虚拟货币都依赖于区块链技术。攻击虚拟货币交易机构,洗劫电子货币的事件将越来越多,损失也会越来越大。

从赋能安全产品的角度,区块链的去中心化、不可篡改和可审计特性,特别适合于强审计需求的领域,我认为基于区块链的征信、溯源系统、操作审计系统将会是个不错的方向。

*本文作者:兜哥,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。