BDCI2017 “人机大战”参赛总结

UPDATE:
妈呀第三名开源了:https://github.com/fuliucansheng/360


update:
比赛第一名的经验分享:https://zhuanlan.zhihu.com/p/33243415


0. 前言:这将是一篇又臭又长的日志

明年就要找工作了。看到自己还是这么菜,心里挺着急的。一直琢磨着找几个比赛做一做,这样到时候简历不至于一片空白。但又总用“学得不够、能力不足”的借口一拖再拖。
决定参赛的时候是十一月初,当时国内主要的比赛有 BDCI 2017 的系列赛、京东金融的比赛、天池和十一月底的滴滴算法。
滴滴的题目看了一下,需要做数学建模,笔者一向头疼这个;京东的“猪脸”题(噱头力 MAX),考虑到初赛十二月初才结束,可以晚几天再做。
另外 BDCI 是 CCF 举办的,总觉着比较靠谱(错觉一)。看完赛题之后,360 的“真假文本分类”似乎是最好上手的(错觉二),所以就是它了。
本文就是简单讲讲参赛经历,再把自己试过的办法做个总结。等大佬们答辩完了,如果有人开源的话,还想再复现一下(Update:直到本文发布也没有开源)。


写作中的 Update:
GitHub 上找到两个仓库,非常惊喜,心想大佬们就是厉害代码比笔者写得工整太多,结果往下一看名次都没笔者高(捂脸。
虽然如此,他们的代码都比笔者的有参考价值。因为说实话笔者当时的分数自己也解释不清,那个模型试了几次居然都没重现出来,不知道是哪的问题。因为这段时间比较忙也就没再顾得上。
所以说这个成绩多少是带点撞大运的意味。后面如果 TOP5 们不开源,学一下这两位的方法也是好的。

仓库地址:
https://github.com/a550461053/BDCI2017-360
https://github.com/kongliang2015/bdci_360

补充一下,a550461053 这位是后面有事没再做。我看了一下他的方案,如果继续做下去应该能进前15吧,至少会在我前面。


初次参赛没拿到什么好成绩(初赛21,复赛17),方法也比较辣鸡,所以本文没有经验、只有教训。
既然是自己总结瞎扯,这篇文章可能会非常非常长又没有营养,还请读者诸君不要期望从本文看到多少类似“高质量上分代码教学”之类的干货。

在正文之前,先说几点感受比较深的。

  • 一直觉得自己菜,总想等“学得更好更深一点”之后再来找比赛打。这个想法基本上是错的。早打比晚打好,打了比不打好;本科打好过上了研再打,研一打好过研二打。实操经验非常重要。
  • 队友很关键。这次全是笔者一个人做的,回头看好像也没做出什么有意义的结果,但那几天真的特别累。有队友事半功倍,可以互相讨论、分工合作、相互鼓励支持。笔者是认识人太少,虽然有大神同学但不好意思打扰人家,所以一时找不到合适的同伙。看了一下前十名的队伍都是至少三个人,多数加满了。
    当然如果自己实力足够请忽略这条。
  • 留出足够的硬盘空间。笔者用的主机是祖传拼装服务器,配置说得过去。但装系统的时候考虑不周,硬盘只挂了块 128G 的 SSD,好家伙,初赛结束的时候大概三四十个 G 没有了。占大头的是中间数据,比如分词文件、np 张量,以及网络模型的存档。模型看参数量级,笔者有几个模型都超过 1G 了。通常来说后期做多模型融合的话至少留出三十个模型的空间。比如复赛第一的队一共出了八十个模型……
  • 前面说过,早打比晚打好,这是因为比赛会给出一个“半真实”的应用场景让你折腾,做题的时候你会发现,很多操作可能跟你学的理论关系不大,甚至是游离于你学过的知识之外的。这能让人多少体会到工业界和学术界之间的巨大 gap。有时候做到一半可能忽然做不下去了,这时候大概需要回到理论的轨道上重新思考一下问题出在哪里。这个反复过程就是能力升级的过程。当然了,这种理论和实践之间的偏差其实说明笔者学得不行,并不是理论有问题。
  • 所以最好还是兼备扎实的基础和过硬的实践能力,前者是指导思想、后者是有力保障。
  • 说到这个 gap 忽然有点儿明白 Andrew Ng. 在他那些课程里的教学理念了……

1. 题目:认真读题是个好习惯

比赛官方链接:http://www.datafountain.cn/#/competitions/276/intro
题目大意是区分“机器写作”和“真实”文本。出于简洁,以下称为“假文本”和“真文本”。

就像考试一样,参与这类比赛首先要做的事情是读题。重点包括数据的格式,提交格式,评测标准、规则,甚至赛程——真有人连截止时间都不看就参赛的。不清楚的地方尽快找客服问清楚,一般都会开放比赛用的微信和 QQ 群。
第二件事情是看数据
数据格式在比赛页面写得很清楚,“#ID\t#TITLE\t#CONTENT\t#LABEL”,样例之间用换行符“\n”隔开。
这样读取数据的脚本就好写了:读取文件全部内容直接用 .readlines(),样例内部用 .strip().split('\t')


1.1 浏览数据

2e4e1c09ec9f1d9fda4fde58929d28f6 焉耆加强社会消防技术服务机构监督管理 进一步规范社会消防技术服务机构管理,营造依法执业更有不具备执业资格人员执业现象的发生、更有不具备执业资格人员执业现象的发生,焉耆大队组织开展社会消防技术服务机构专项检查工作,督促社会消防技术服务机构充分发挥工作职能,不断提升建筑消防设施、器材完好率,整合多方力量全力维护全地区火灾形势的持续稳定。焉耆大队党委高度重视,结合辖区成立专项检查领导小组,重点针对辖区内从事建筑消防设施维护保养、检测的单位资质、办公场所、设备、人员等方面逐一进行实地检查,对社会单位执业情况进行核查,组织对辖区设有建筑消防设施三化单位的逐一核查维护保养情况,对照年度消防设施检查消防技术服务活动与取得许可资质的一致性,检查工程项目及社会单位检测报告、维保报告与实地情况的一致性,检查维保记录能否真实反应自动消防设施的设置和维护保养状况,检查消防技术服务机构履职等情况。焉耆大队将消防技术服务机构的监督管理工作作为日常防火工作重要内容,结合建筑消防设施”三化”达标建设工作,对消防技术服务质量实施”三同”,重点落实”三必查”(即一查消防技术服务活动与取得许可资质的一致性,严厉查处未取得资质、挂靠资质、冒用他人名义或超资质范围从事社会消防技术服务活动的违法行为;二查工程项目及社会单位检测报告、维保报告与实地情况的一致性,核查报告真实性,严厉查处恶意检测、维保漏项或检测结果失实,出具虚假或者失实文件的违法行为;三查消防技术服务机构履职情况,严厉查处无资格人员执业,按照一事一案直至降低或取消其资质、资格,督促社会消防技术服务机构规范执业行为、提升服务质量,加强消防技术服务机构监督管理,充分发挥社会消防专业力量,大力推进消防工作社会化管理、提升社会防控火灾能力。专项整治之前,社会服务机构责任心不强,工作不深入,更有不具备执业资格人员执业现象的发生,到单位检查也会存在刚刚维保过的单位,设施就存在问题,单位负责人评价不高。经过一段时间的专项整治,社会单位纷纷反映消防服务机构服务水平大幅提升,能够认真履行职责,等进行曝光,为单位隐患整改提供技术支持。焉耆队严格按照总队《社会消防技术服务机构监督管理办法》和《社会消防技术服务行业信用评价管理办法》要求,加强社会消防技术服务机构服务质量诚信体系建设,对监督管理中发现的违法违规行为、被实施行政处罚情况等不良行为信息,通过报刊、等进行曝光,对不合格等次的社会消防技术服务机构,提请支队对其暂停其消防行业自律管理系统使用权限。检查工程项目及社会单位检测报告,焉耆大队消防技术服务机构进行专项检查工作正在开展中,实地抽查7家消防技术服务机构,并对1家消防技术服务机构进行立案行政处罚。 NEGATIVE
ba211cf520cd51623b87b6939e16053c 工人失足摔伤 贵州长顺消防成功营救 图为救援现场7月12日17时21分,贵州省长顺县消防接到群众报警称:在长顺县潮顺洗车场旁边的建筑工地上有一名工人在作业时不慎坠入房间中,房间四周无门窗,无法将工人救出,请求消防队前往救援。接到报警后,消防官兵迅速出动前往救援。17时28分,官兵到达现场后发现一名工人在作业时失足踩空,落入一个四周无门窗的房间内,腰部受伤无法正常行动。根据现场情况,消防官兵立即展开救援,由两名官兵下到屋内使用多功能担架固定并保护好被困人员,再与周围群众配合使用绳索将被困人员救出。经过一个小时左右的紧张救援,18时32分,被困人员被成功救出并移送现场医护人员。险情处置完毕后,消防官兵随即撤离现场。 POSITIVE

这里打印了训练集(train.tsv)的前两条样本,不妨来人工检查一下问题在哪。
(检查了一部分数据,群里大佬也有一些截图,看起来“正例”基本上都是网上爬取的各种新闻和广告。360似乎认为爬取所得的文本都是“真文本”,而算法生成或者用一些手段做出来的都是“假文本”。这样就出现了一些很好玩的情况,后面会列几个例子。)

第一条材料乍一看还挺“真”的,因为非常符合一般人印象中的公宣文模板。但仔细观察会发现问题很多。
仅在首尾两句话,根据脑子里所剩无几的中学语文知识,找到了以下几点问题:

  • 开头缺少一个连接词,如果按正常句法根据文意补全,“进一步”前面应有一个“为了”。
  • “更有不具备”这一句重复。(这一条很像 RNN 做文本生成会出现的 bug。)
  • “检查工程”半句与后面“大队如何如何”的连接明显有问题。

那么从第一条负样本能得到什么信息呢?

  • “明显的语病”可能是重要特征
  • “机械式的重复”应当是重要特征(不过在复赛时被新数据集 hack 掉了)
  • “文段含义的连续性”可以作为重要的判断依据

这三条不难理解,仔细考虑一下其实人在判断文本真假的时候,也会隐性地想到像这样的一些规则。

1.2 最初思路

回到题目上。在写处理脚本之前先扔了个 randomGuess 上去。比赛评分按 F1,即

score=2precisionrecallprecision+recall. s c o r e = 2 ∗ p r e c i s i o n ∗ r e c a l l p r e c i s i o n + r e c a l l .

不难想到,如果正负样本数相近且在测试集上随机排列,不妨假设测试集服从 p=0.5 p = 0.5 01 0 − 1 分布,这样设阈值为 0.5 做随机猜测,可以得到接近 0.5 的成绩。需要注意,数据挖掘比赛里,如果测试集排列是有规律的,这属于“数据泄露”。正规比赛不应该出现这样的问题。
试了一下,得分 0.44 ,重复试验结果也差不多。这样看来,正负样本可能稍微有些不均衡。
对训练集做了一下统计,正负数量之比大概是 36:64。把这个比例带入,与测试集的分数反馈相差不大。
通过这样的猜测、尝试和分析,可以得出以下结论:

  • 正负样本不均衡,数量比约是36:64。
  • 测试集与训练集的样本比例接近。

这两条结论有一定的参考价值,因为比赛的最主要判定标准是分数,创意和具体的算法评价要等到决赛才有用。这就决定了参赛者的核心目标就是“上分”。一些根据分数计算公式来略微提高分数的方法,这种工作属于比赛内容的一环,不算作弊。
另外估算样本比例,也能在后面的算法中改善判定结果。比如你用神经网络来做这个题,输出是 softmax 给出的 1×2 1 × 2 向量,向量的值可以理解为网络对输入在两个类别上的预测概率,那么在写输出标签的时候可以根据样本比例微调判定阈值。这样做确实是有效果的。

如果从经验、规则学习的角度来看,上面的三条规则都可以写到算法里。不过笔者对这方面毫无了解,不知道怎样才能快速挖掘和实现。进决赛的前五名里有一位做了这些工作,希望后面能看到他的开源。

虽然没学过 NLP,上半年同学大佬带着做了一个练习赛,大概知道一些传统的做法,比如词袋、词频和 TFIDF 之类的。对一些典型任务,TFIDF + SVM 已经可以达到比较不错的效果。但比赛群里有人说试过这个套路,效果一般(0.6~0.7)。
在 Deep Learning 横扫 CV、Speech、NTM 的今天,很多人认为 NLP 会是下一个被攻克的领域。比如对文本分类任务来说,2014年出现的 TextCNN 是一个非常不错的模型。网络结构非常简单,层数也不深,在很多数据集上取得了可观的准确率。
至于竞赛方面,知乎“看山杯”最后,获奖队伍都使用了深度神经网络。比赛群里官方人员也明确说了,传统方法可能不行,你们最好考虑 Deep Learning。

开赛一段时间后,官方给出了一个 word2vec + fasttext 的方案[1],可以拿到 0.73 的分数。说起来非常搞笑,记得当时初赛 A 榜高于这个分的不超过四十个,也就是说用 baseline 可以稳进复赛(复赛入选取初赛的前100名)。这个数据可能说明了这届 BCDI 的实际参与程度吧。
初赛 A 榜笔者没有继续提交。不妨作为参考:初赛 B 榜时这个提交下降到了 0.69,笔者在初赛的最终得分是 0.73,排21。

在看到官方 baseline 之前笔者是一头雾水,不知道从哪开始做起。之前做过的那个小比赛,琢磨一下两者的套路还是相当不同的。但有了官方 baseline ,又参考了一些资料[2],笔者觉得似乎也不是不能做。上面提了 TextCNN,接下来的时间都花在了调教这个网络上面。[2]提到的其他方法也试了一下效果很不好,可能是因为对模型理解不到位,超参数给得不对。
TextCNN 代码非常简单,网上用 tensorflow、pytorch、keras、mxnet 各种框架写好的开源遍地都是。另外看山杯也有开源资料,不过笔者觉得那些东西对没做过看山杯的人来说价值不大,太杂乱了,可能思路上的启发会有。不如去看些博文啊、GayHub 开源啊之类的有意义。[3]

1.3 玩具代码

如果还没被混杂了各种乱七八糟内容的上文搞晕,你大概记得笔者写过一条后来被 hack 掉的规则。
深度网络生成模型出 bug 的时候可能会重复输出一段文字,这个特征可以很直接地使用在测试集上。所以当时有人在群里传了一个非官方 baseline,拿来一试居然也有超过 0.5 的分数(忘了是 0.61 还是 0.67 ,笑死。


写作中的 Update:
后来在开头提到的第一个仓库里找到了这个 baseline,这下对上号了。感谢 dalao 的分享。


# -*- coding: utf-8 -*-
"""
Created on Sat Oct 14 20:56:14 2017

@author: qiaosj
"""
trainfile="./evaluation_public.tsv"
with(open(trainfile)) as f:
    lines = [line.strip().split('\t') for line in f.readlines()]

import pandas as pd  
df=pd.DataFrame()
ids=[]
tcount=[]
r=[]
for line in lines:
    ids.append(line[0])
    if len(line)==2:
        line.append('')
    dtl=line[2].replace(' ','').replace(',',' 。').replace('、',' 。').split('。')
    pool=[]
    t=0
    length=0
    for i in dtl:
        if len(i) < 7:
            continue
        length+=1
        if i not in pool:
            pool.append(i)
        else:
            t+=1
    tcount.append(t)

df['id']=ids
df['space']=tcount
df['label']='NEGATIVE'
df['label'][(df['space']==0)]='POSITIVE'

result=df[['id','label']]
result.to_csv('./test.csv',index=False,header=False)

len(i) 的阈值设成 7 了,因为试了几次好像这个数比较高。

2. 官方 baseline 实现:用 baseline 居然能进前四十……

赛后反思,这两三个星期里其实没做多少事情。主要就是两个:实现官方 baseline 和 textCNN 的调参。
官方 baseline 还是挺友好的,关键代码都给了,笔者这个小白都能很快实现出来。具体实现可以分为四个部分:

  • 分词
  • 训练 wordvec
  • 用 fasttext 做监督学习
  • 使用学得的模型做分类,并按要求格式化提交文件

下面分别做几点简单说明。

2.1 分词

官方用了 jieba 做分词,笔者试了 thulac,得分有微小的提升。但不知道是不是用法不对,速度相差很多,jieba 分完一次大概是二三十分钟, thulac 却花了两三个小时。这跟他们官方页面上的描述出入挺大的,怀疑是 API 调用上有瓶颈,因为跟360官方的代码一样,都是逐个样本做分词的。
这里说句题外话,理论上讲,用不同的分词结果可以做模型融合,不过连这种微小的提升潜力都不放过,多少有点“夺泥燕口、削铁针头”的意思,不管是在比赛后期还是在工程实现上,既没意思、也没意义,只是比赛手段而已。

分词代码举例:

# 训练数据处理
with open(trainfile, 'r', encoding='utf-8') as f, \
    open(Traintext, 'w', encoding='utf-8') as output:
    for line in f.readlines():
        l_ar = line.strip().split('\t')

        if len(l_ar) != 4:
            continue    # 忽略格式有问题的样本
        id      = l_ar[0]
        title   = l_ar[1]
        content = l_ar[2]
        label   = l_ar[3]

        seg_title   = jieba.cut(title)
        seg_content = jieba.cut(content)

        r = " ".join(seg_title) + " " + " ".join(seg_content)

        output.write("__label__" + label + " " + r + '\n')

# 测试数据处理
with open(testFile, 'r', encoding='utf-8') as tf, \
    open(testSave, 'w', encoding='utf-8') as tS, \
    open(idSave, 'w', encoding='utf-8') as iS:
        for line in tf.readlines():
            line_seg = line.strip().split('\t')
            id    = line_seg[0]
            title = line_seg[1]
            content = line_seg[2]

            seg_title = jieba.cut(title)
            seg_content = jieba.cut(content)

            r = " ".join(seg_title) + " " + " ".join(seg_content)

            print(id, file=iS)
            print(r,  file=tS)


2.2 训练词向量

官方没给出来的是 word2vec 生成词向量的操作。这块网上有一些用 gensim 实现的,笔者用的是 Google 官方开源的 C++ 版。word2vec 的超参数设置据说水很深,笔者只试过几组设置,结果相差不多,也不好说哪些更合理,建议不熟悉这块的朋友先尝试一部分默认的参数组合。
另外,比赛要求不能用外部数据,用外部数据训练的词向量也不应该使用,只能从训练集、测试集里选择语料。于是考虑训练词向量的本意——“为文本的 one-hot representation 寻找一种合理的 distributed representation ”——那么要学习的应该是“真实文本的表示形式”。这样,选择训练集中的正样本作为训练语料是合理的。
但问题又来了。理想的情况下,word2vec 的训练样本越大越好,而“训练集中的正例”相对于要处理的数据来说实在太少了(前面也提到,正样本只占约36%),所以训练出的词向量肯定不够好。举个例子,笔者训练出来的词向量有大约三十八万个词,而训练集加测试集一共有不到二百五十万词。
与之对照,看山杯提供了转码版的 Google 词向量,那是 TB 级的语料训练出来的词向量。
而且 dalao 们在群里讨论过,对这个题来说 word2vec 的确不是很重要,排名第一的队伍根本就没有用。即仅就本题而言,没有必要过多纠结在词向量的训练上。

训练词向量的命令举例:

./wordvec -train segName.txt -output output.bin -cbow 0 -size 100 -window 5 -negative 0 -hs 1 -sample 1e-4 -threads 40 -binary 1 -iter 30

提交得分跟官方的差不多。

2.3 FastText

FastText 是 Facebook 给出的一种用于文本分析的快速实现方案,把一些 NLP 的经典思想和浅层网络组合起来了,优点是速度非常快,比深度网络快了十倍甚至百倍以上,却能得到可以接受的精度。
FastText 是 C++11 实现的。依然是 git clone 然后编译,也有比较多的参数可以调。据说只用 fasttext 可以刷到 0.80 左右(初赛前十五)。可惜不会(捂脸。

训练和预测样例:

lr=0.1
epoch=100
dim=200
bucket=5000000
model_name=model_ai
input=./res/somefile.txt
w2v=./res/w2vBig.bin

./fasttext supervised -input $input -output $model_name -pretrainedVectors $w2v -lr lr -epoch $epoch -wordNgrams 3 -bucket $bucket -dim $dim -loss hs -minCount 1 -thread 32


$predict=./fttest.txt
./fasttext predict $model_name".bin" $predict > ./predict.txt


2.4 输出格式化

这一步是因为 FastText 的输出跟提交格式不一样。在做分词的时候把测试集数据的 id 另外保存了一份,这样在得到上面的 predict.txt 之后,就可以直接组合出提交文件:

import pandas as pd
submit_csv = 'result.csv'
label_predict = 'predict.txt'

ids = []
labels = []
with open(idSave, 'r', encoding='utf-8') as iS:
    for line in iS.readlines():
        ids.append(line[:-1])

with open(label_predict, 'r', encoding='utf-8') as lP:
    for line in lP.readlines():
        labels.append(line.split('__')[2][:-1])

print((ids[1]))
df = pd.DataFrame()
df['id'] = ids
df['label'] = labels
df.to_csv(submit_csv, index=False, header=False)

需要注意,数据竞赛的提交是非常关键的环节,每次提交之前最好做一下检查。
正规比赛中,每天的提交次数是有限制的,这是为了防止有人注册大量小号多次提交来推测正确答案——有人可能会记得某一年百度在某国际比赛上闹出的丑闻,当时那个部门的负责人都为此引咎辞职了。
DataFountain (本次比赛的承办方)的平台槽点非常多。
想到这个事就非常窝火,必须专门挑出来批判一下。

刚报名那时候连续三次提交都报“逻辑错误”,翻遍了官方网站能找到的地方都没有看到对这个报错的解释。在一千多号人的群里问没人回答,官方客服也不见踪影(后来才知道官方客服除了上线发通知,一天出现那么几次,摊手。
连续错误提交后当天就不能提交了,当时气得差点退坑。过了大半天才有好心人说“你可能是正负样本的比例不对”。这才反应过来,检查了一下预测正样本居然只有10%,而根据最开始的推测大概要 35% 左右,所以之后直接写了个检查结果的脚本以防万一。


3. TextCNN 实现和调参:新手炼丹师的地狱之旅

这是耗时最多的部分。赛后反思,觉得像这种自己瞎琢磨乱试还是学不到多少东西。没有理论指导的情况下,沉迷于尝试各种超参,就算最后调教出一个神奇的网络,实际上并没有什么意义。
代码是参考[2]修改出来的,为了省时间直接用 Keras 来搭。

3.1 准备工作

现在手头的材料有:

  • 训练集和测试集文本的分词(存在两个 .txt 里)
  • 训练集标签(.txt
  • 词向量模型(.bin

虽然说 Deep Learning 是一种“端到端”的方法,但把文本直接输入网络,现在还是做不到的。参照[2]的方法做了 one-hot 映射,因为工具在 Keras 里是现成的。one-hot 并不只有 [0 0 ... 0 1 0 ... 0] 的形式——对文本来说,这样做开销太大了——还可以用一个 dict 把所有词汇保存下来,扫描语料的时候记录一下词频,完事排个序,用序号来表示词汇,这样就形成了一个从词汇到整数的映射。这种方法也叫多项式表示(multinomial representation),与 0-1 的 binomial representation 相对应。
比如一句话:“日志字好多啊不想写。”(人工)分词后是“日志 字 好多 啊 不想 写 。”这里没有重复的词,所以可以直接按顺序添加到词典中:

{'日志': 1, '字': 2, '好多': 3, '啊': 4, '不想': 5, '写': 6, '。': 7}

这样,原句就可以表示为一个整数构成的向量:[1, 2, 3, 4, 5, 6, 7]
类似地,“不想写日志”经过分词和映射后就变为 [5, 6, 1]。顺便,这样就有了一个问题,每条材料的词数几乎都不一样,但后面要输入网络的数据的维度肯定是相同的,这就需要做一下统一。一般是直接截短/补长,即人工设定一个长度,比它长就扔掉后面的,比它短就在最后用 0 补齐。


3.1.1 数据统计和简单可视化

所以问题来了,这个长度设多少合适呢?一开始自己估摸着随便选了一个数。1000 挺长了吧?可惜这么搞没谱。后来做了个简单的统计和可视化,结果是这样的:
这里写图片描述

这是正样本的长度分布。
这里写图片描述

这是负样本的长度分布。

不难看出来,正负样本在长度上好像稍微有点区别。这里为了图形的比例,只显示了频次前 80% 的长度,有一些极端的文本特别地“右”。
有意思的是,负样本这边,大概3000词是一道坎,有一个奇怪的陡降。这与 RNN 在生成长文本上的缺陷似乎有一些关系。
不过在后面的模型中把长度降到了200,影响也不是很大,所以……
这里的画图只是个辅助工具而已,心里有数就行了。

3.2 数据处理

接下来做一些处理。为了偷懒直接用了 Keras 的套件:

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences

tokenizer = Tokenizer(MAX_NB_WORDS)
tokenizer.fit_on_texts(all_data)
word_index = tokenizer.word_index
sequences = tokenizer.texts_to_sequences(all_data)

这里的 all_data 是训练和测试文本组合出一个巨大 list,因为两边要一起处理。MAX_NB_WORDS 是个限制 texts_to_sequences 的参数,word_index 不妨用 json 保存起来备用。
这段代码的意思是,先统计一下语料中的信息然后把分词数据转换为多项式的 one-hotword_index 按词频存储了一个形如上方 {'日志': 1, '字': 2, '好多': 3, '啊': 4, '不想': 5, '写': 6, '。': 7} 的巨大词典,值大于 MAX_NB_WORDS 的词会被写 0

print('Found %d unique tokens.' % len(word_index))

Found 2729771 unique tokens.
一共找到了二百七十多万的分词结果。然后是截短和填充:

train_data=pad_sequences(sequences[:trainLength], maxlen=MAX_SEQUENCE_LENGTH)
test_data=pad_sequences(sequences[trainLength:], maxlen=MAX_SEQUENCE_LENGTH)

pad_sequences 返回的是 np.array,当前的 np.array 直接占用内存,笔者内存不够,所以直接在这里把它们分开了。MAX_SEQUENCE_LENGTH 是控制数据长度的参数,这里取了1000。其实后来发现完全可以更短,长度也不是关键。

最后把标签表示成 one-hot 的形式。这里要用在网络的输出端,所以就是 0-1 了。

from keras.utils import to_categorical
labels = to_categorical(np.asarray(train_label))

在读取数据集做分词的时候就已经把原来的 POSITIVENEGATIVE 保存为 01 了。这里做的不过是把整数变为向量而已。
现在输入网络的数据已经有了。在训练网络之前,还要打乱并分出训练、验证集。这里用的是9:1,但其实对于几十万级的数据,笔者觉得可能99:1就够了,注意分之前打乱一下就行。

VALIDATION_SPLIT = 0.1

valIdx = int((trainLength)*(1-VALIDATION_SPLIT))
idx = np.random.permutation(trainLength)
train_data = train_data[idx]
labels = labels[idx]

x_train = train_data[:valIdx]
y_train = labels[:valIdx]
x_val = train_data[valIdx:]
y_val = labels[valIdx:]

x_test = test_data

为了方便后续使用,最好把这些 Numpy Array 存一下:

npSaveFile='./nptemp.npz'
np.savez(npSaveFile, x_train=x_train, y_train=y_train, x_val=x_val, y_val=y_val, x_test=x_test)

这样,之后使用的时候只需要:

import numpy as np
npSaveFile='./nptemp.npz'
data = np.load(npSaveFile)

x_train = data['x_train']
y_train = data['y_train']
x_val = data['x_val']
y_val = data['y_val']
x_test = data['x_test']


3.3 网络结构

这部分内容请批判性地阅读。在没看到 TOP5 开源的情况下,笔者对以下所有参数的设定没有任何把握。
另外可能有的内容前后对不上。没办法,笔者把大量的中间数据给删了,复现起来非常麻烦就懒得做了。很多数字是从遗留的 Notebook 直接拿过来的。

3.3.1 嵌入层

最开始使用的是预训练词向量textCNN 的结构,这部分依然参考了[2]:

VECTOR_DIR='./data/w2v.bin'
EMBEDDING_DIM = 200

import json
with open('wordidx.json', 'r') as wi:
    word_index = json.load(wi)

import gensim
from keras.utils import plot_model
w2v_model=gensim.models.KeyedVectors.load_word2vec_format(VECTOR_DIR, binary=True)
embedding_matrix=np.zeros((len(word_index)+1, EMBEDDING_DIM))
not_in_model=0
in_model=0
for word, i in word_index.items():
    if word in w2v_model:
        in_model += 1
        embedding_matrix[i] = np.asarray(w2v_model[word][:EMBEDDING_DIM], dtype='float32')
    else:
        not_in_model += 1

print(str(not_in_model) + ' words not in w2v model')

这段代码建立了一个 len(word_index) * EMBEDDING_DIM 维的空矩阵 embedding_matrix,从训练好的词向量中取出权值填充到 embedding_matrix 中。这个映射矩阵将作为网络最前端的嵌入层使用。
嵌入层(Embedding Layer)的作用在 Keras 的文档里有很简明的解释:把整数序列(如[20, 46])映射为实数([0.3, 0.46])的网络层。
这是因为输入序列长这样子:

array([  4104,    276,    146,  19639,      2,  24848,    339,  74067,
            4,  17770,      4,   9485,     18,      3,   2407,    875,
        17825,   2407,  90875,     55,    285,      1,  11636,   5094,
        ...
        2,   9571,      1,    886,   2510,   2603,   6264,      3], dtype=int32)

不同于维度受限的图像数据,用整数表达文本序列是一种比较低效的方法。为了更紧凑地表示输入序列的信息,就需要有一个合适的映射关系。word2vec 训练出的词向量就是一类比较常用的工具。

然后就可以形成网络的嵌入层了:

from keras.layers import Embedding
embedding_layer = Embedding(len(word_index)+1, EMBEDDING_DIM, weights=[embedding_matrix], input_length=MAX_SEQUENCE_LENGTH, trainable=False)

可以看到,上面的映射矩阵作为权重填入了嵌入层中。trainable 参数将控制网络在训练过程中的行为,False 选项表示在训练网络时,本层权重不作改变。

3.3.2 textCNN

关于这个话题,网上有很多非常好的资料,这里就不列举了。
Keras 搭 textCNN 是非常简单的:

from keras.layers import Embedding
from keras.layers import Dense, Input, Flatten
from keras.layers import Conv1D, MaxPooling1D, Embedding, Concatenate, Dropout, BatchNormalization
from keras.models import Model
convs = []
filter_sizes = [3,4,5]

sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32')
embedded_sequences = embedding_layer(sequence_input)
for fsz in filter_sizes:
    l_conv = Conv1D(128, fsz,activation='selu', padding='same')(embedded_sequences)
    l_pool = MaxPooling1D(5)(l_conv)
    convs.append(l_pool)
l_merge = Concatenate()(convs)
l_drop1 = Dropout(0.5)(l_merge)
l_cov1= Conv1D(256, 5, activation='selu', padding='same')(l_drop1)
l_pool1 = MaxPooling1D(5)(l_cov1)
l_flat = Flatten()(l_pool1)
l_dense = Dense(64, activation='selu')(l_flat)
preds = Dense(2, activation='softmax')(l_dense)
model = Model(sequence_input, preds)
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['acc'])
model.summary()

.summary() 可以非常体贴地打印出网络的结构信息。你可以看到网络的结构、每一层的参数数量、总的参数量。比如:

Layer (type)                    Output Shape         Param #     Connected to  
==================================================================================================
input_4 (InputLayer)            (None, 1000)         0                                            
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, 1000, 100)    272977200   input_4[0][0]                    
__________________________________________________________________________________________________
conv1d_16 (Conv1D)              (None, 1000, 128)    38528       embedding_1[3][0]                
__________________________________________________________________________________________________
conv1d_17 (Conv1D)              (None, 1000, 128)    51328       embedding_1[3][0]                
__________________________________________________________________________________________________
conv1d_18 (Conv1D)              (None, 1000, 128)    64128       embedding_1[3][0]                
__________________________________________________________________________________________________
max_pooling1d_16 (MaxPooling1D) (None, 200, 128)     0           conv1d_16[0][0]                  
__________________________________________________________________________________________________
max_pooling1d_17 (MaxPooling1D) (None, 200, 128)     0           conv1d_17[0][0]                  
__________________________________________________________________________________________________
max_pooling1d_18 (MaxPooling1D) (None, 200, 128)     0           conv1d_18[0][0]                  
__________________________________________________________________________________________________
concatenate_4 (Concatenate)     (None, 200, 384)     0           max_pooling1d_16[0][0]           
                                                                 max_pooling1d_17[0][0]           
                                                                 max_pooling1d_18[0][0]           
__________________________________________________________________________________________________
dropout_7 (Dropout)             (None, 200, 384)     0           concatenate_4[0][0]              
__________________________________________________________________________________________________
conv1d_19 (Conv1D)              (None, 200, 256)     491776      dropout_7[0][0]                  
__________________________________________________________________________________________________
max_pooling1d_19 (MaxPooling1D) (None, 40, 256)      0           conv1d_19[0][0]                  
__________________________________________________________________________________________________
conv1d_20 (Conv1D)              (None, 40, 256)      327936      max_pooling1d_19[0][0]           
__________________________________________________________________________________________________
max_pooling1d_20 (MaxPooling1D) (None, 8, 256)       0           conv1d_20[0][0]                  
__________________________________________________________________________________________________
dropout_8 (Dropout)             (None, 8, 256)       0           max_pooling1d_20[0][0]           
__________________________________________________________________________________________________
flatten_4 (Flatten)             (None, 2048)         0           dropout_8[0][0]                  
__________________________________________________________________________________________________
dense_7 (Dense)                 (None, 64)           131136      flatten_4[0][0]                  
__________________________________________________________________________________________________




dense_8 (Dense) (None, 2) 130 dense_7[0][0] ================================================================================================== Total params: 274,082,162 Trainable params: 1,104,962 Non-trainable params: 272,977,200 __________________________________________________________________________________________________

这个结构是最开始用的一个结构,可以看到参数总量达到了两亿七千万,可训练参数有一百一十万。深度网络的超参数选择暂时是个缺乏可靠理论指导的问题,多数情况下只能凭借经验来尝试。Andrej Karpathy 在讲 charRNN 的时候提到一些经验,大体上是参数不要超过样本数太多。
笔者自己在调试的时候也发现了这个问题。样本一共有五十万,那么网络可调参数的上限设在一百万左右即可。作为参考,多次实验中可调参数量在一百二十万左右时,网络就发生了过拟合。

现在数据也有了、模型也有了,可以开始训练了。训练的代码很简单,只要一行:

model.fit(x_train, y_train, epochs=1, validatio_data=(x_val, y_val), batch_size=batch_size)


3.3.3 Batch Size 、网络结构与参数量

搞深度学习,内存还是越大越好。显存没办法了,TITAN V 也不过12G显存。内存和显存有限,所以在做大量数据的处理时就容易内存不足。比如上面两亿七千万的参数量,按 float32 计算就是十亿字节,超过1GB内存占用。笔者使用了 GPU, 所以这部分参数会在显存里做计算。
好歹也是几十万的样本,肯定不能做 Batch Training。Mini-batch 的数量就要注意了。另外网上没有找到说明,印象中 trainable=False 之后这一层参数是不在 tensorflow 的计算图里的,也就是说不会占用显存空间。
所以你可能看到一些炼丹家们时不时地就打开计算器算一下。他们是在算显存和内存够不够用呢。

那么问题来了,不够用的话怎么办?

  • 早期的 AlexNet 网络做得比较大,一个显卡只有 3G 显存,所以他们当时很多工作是在尝试两块显卡训练网络。现在的深度学习框架有的可以做比较细化的分配,比如 tensorflow 里可以指定使用哪块显卡。这是一个办法。
  • 网络结构也可以调整。上面的网络 untrainable 的部分冗余太多了,接近三个亿的参数(虽然绝大多数冻结了)你是想干什么?在这个网络里,大部分参数其实就是嵌入层的参数,是两百七十万单词乘一百维词向量的结果。但训练好的词向量其实只存了大概三十八万个词,还有二百三十万个词的位置上是空的。这样显然有问题。
  • 再就是 batch_size。上面直接写了 .fit,但如果没记错的话当时是爆了显存的。后来就改成了 .fit_generator,不过这就需要你手写一个 batch_generator,也不难。Pytorch 里的 DataLoader 机制就把这一步明确化了。
3.3.4 细枝末节

截一个初赛的训练结果:

Epoch 1/1
878/878 [==============================] - 239s 272ms/step - loss: 0.2722 - acc: 0.8789 - val_loss: 0.4216 - val_acc: 0.8248

当时能出来这么一个模型还是挺高兴的。不过这前面还有几个 epoch,因为网络容易过拟合,所以只能一轮一轮地尝试。后来用了 Keras 里的 earlyStoppingcheckpointer把这个过程自动化了,但是再也没出来这么好的拟合结果(哭了。

模型训练完最好保存一下。上面自动保存的机制可以用,手动保存也很简单:

model.save('fit_cnn.h5')

读取只要:

from keras.models import load_model
model = load_model('cnn.h5')

预测也只需要一行代码:

predict=model.predict(x_test, batch_size=1024)

还记得之前的样本比例问题吗?网络输出的是(0,1)区间内的实数值,在划阈值写标签之前最好检查一下:

np.sum(predict, axis=0)/predict.shape[0]

这里保存的结果是

array([ 0.71660437, 0.28345084])

还凑合。阈值这里取了0.4,也是考虑到样本比例的偏差。不过主要是打完输出文件之后再检查发现 0.4 给出来的预测比例更好:

sizepre = predict.shape[0]
for i in range(sizepre):
    if predict[i][3] > 0.4:
        label='POSITIVE'
    else:
        label='NEGATIVE'
    predict_labels.append(label)

3.4 弃用词向量

0.73 混入了复赛就琢磨着怎么继续做下去。初赛模型到了复赛只有大概 0.67,复赛数据的难度是有提高的,不信待会截图给你看(手动滑稽)。考虑了很多办法,也试了 LSTM。但复赛刚开始那段时间真是黑暗,好像所有的方法都失效了。不管 textCNN 还是 LSTM 都拟合不了模型,具体表现就是 loss 下降到某处突然起飞,或者直接一开始就起飞,准确率最后稳定在 60% 左右。这样预测出来的模型就是全负,因为在拟合不出模型的情况下,网络会自动给出一个“无可奈何的解”,即全部预测为比例高的那一方。
当时真的百思不得其解啊,因为参数量绝对是够的。
之前刚好拜读过 ICLR 2017 的最佳论文《Understanding Deep Learning Requires Rethinking Generalization》,里面有一条结论就是只要结构没有问题的情况,把参数设得足够大,网络是可以随意拟合模型的。
笔者最初用的还是词向量嵌入+ textCNN,嵌入层锁死,只改变网络中的参数,活动参数一百多万。train 不动。
后来直接把嵌入层打开,这下哦买噶der,两亿参数该够了吧?还是在欠拟合。

这特N的什么龟龟!!!

现在觉得可能还是学习率太大。前面一直用的是 adam 的默认学习率(lr=0.01),训练得很好就没动。还是网络跑得不够多,之前做的东西, learning rate 基本都用默认参数。但很多实际问题上,尤其是在第一次接触某一类问题的时候,可能确实需要先做个随机网格搜索来找一个合适的初始值。
不过当时没想这么多,只是怀疑是不是网络结构不行。上面给的代码中间改过好几个版本。最初用的是[2]里的 textCNN,它是把原版在后面加了两个卷积层。因为笔者菜嘛,也不知道到底该怎么改,就按照这个思路加加减减,最后发现没什么卵用(捂脸。这才在最后改回了原版的 textCNN。
那么问题后来是怎么解决的呢?

从头开始 train。词向量也不要了,语料长度继续截短,参数量控制一下。中间笔者又读了一下那篇讲 textCNN 调参的文章[4],对最优 kernel_size 做了一下搜索。改后的代码样例如下:

from keras.layers import Input, Conv1D, MaxPooling1D, Dropout, Flatten, Dense,ZeroPadding1D, Concatenate, Embedding, GlobalMaxPooling1D, BatchNormalization
from keras.models import Model
from keras.layers.advanced_activations import PReLU

convs = []
filter_sizes = [2,2,2,3,3,4,5]

sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32')
embedding_layer = Embedding(20000+1, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH, trainable=True)
embedded_sequences = embedding_layer(sequence_input)
for fsz in filter_sizes:
    l_conv = Conv1D(100, fsz, padding='same', activation='selu')(embedded_sequences)
    l_pool = GlobalMaxPooling1D()(l_conv)
    convs.append(l_pool)
l_merge = Concatenate()(convs)
l_drop1 = Dropout(0.2)(l_merge)
l_dense2 = Dense(64, activation='selu')(l_drop1)
l_bn2 = BatchNormalization()(l_dense2)
l_drop3 = Dropout(0.0)(l_bn2)
preds = Dense(2, activation='softmax')(l_drop3)

model = Model(sequence_input, preds)
model.summary()

大半个月过去,很多细节现在已经记不清了。网络外超参数包括输入长度可能是 200 或者 500,嵌入层试过 5000*200200000*100,用不用预训练好的词向量这些。总的来说,主要发现包括:

  • 预训练词向量对本题用处不大
  • 词空间(嵌入层尺寸)也不需要太大
  • kernel_size 从2到5,拟合速度都比较快,1不行,大于5之后效果也欠佳
  • textCNN 原始结构就很有效,不需要后面再叠卷积层
  • 参数量需求不大,一百万到一百二十万即可拟合训练数据
  • dropout 非常有用,仔细调
  • batch-normalization 试了才知道有没有效果
  • 激活函数不是特别重要,倒不如说依赖特定激活函数的网络,泛化性可能很差(最近有篇论文据说就有这个问题)

第二点可能需要解释一下。前面说过一点文本的表示方法和嵌入层的初始化,两者都受到一个 MAX_NB_WORDS 参数的控制。如果使用预训练词向量,可以在初始化嵌入层时丢弃序数大于该参数的词向量;如果不使用预训练词向量,像上面代码所写的,Keras 会自动忽略序数大于 20000 的内容。

3.5 调参感言

接下来就是漫无止境的调参了。缩减输入长度和嵌入层尺寸之后,网络终于开始学到东西了。但绝大多数情况都是很快过拟合,分界点大概是验证集准确率到70%的时候,训练 loss 和准确率继续下降、上升,验证 loss 和准确率开始上升、下降。说明网络结构还是有问题。
然后比赛中最搞笑的一幕出现了,突然调到了一个参数,验证集准确率上升到76%,这时候训练集大概是80%左右,看到之后赶紧把模型保存了一下,然后稍微改动了几个参数,又崩了。然后想改回到这个模型再做微调的时候,发现回不去了!

神TM回不去了!

真的,非常诡异的事情。model.summary() 可以看大部分超参,所以排除之后笔者觉得问题应该出在 dropout 上。笔者记得当时给了两个值,第一个 dropout 设的是0.2, 第二个设的是0.4,感觉所有的参数都跟上次的一模一样,但是就出不来那个结果了……
所以笔者那个恨啊。当时就想弃赛了。
再往后几天确实没有做什么,随手调了几次参数发现不灵,于是直接放弃。

4. 反思

4.1 一点调参的经验

调参是个脑力+体力活。调参需要有一些方向性的指导思想,无脑调参的话非常耗时间。好一点的显卡是非常必要的,它能缩短等待的时间,让你尝试更多思路。
关于调参,笔者觉得有一些次序可以作为参考:

  • 最重要的是网络结构,结构不对都白搭。用什么结构参考相关论文。
  • 选好结构,先用比较多的参数——比如样本的十倍——试试能不能过拟合,过拟合是 training 可行与否的标志
  • 过拟合之后开始缩减参数。减少到原先的一半,如果还能过拟合,就使用 dropoutbatch_normdropout0.5 开始调。
  • 或者不削减参数,把初始学习率调得足够小,轮数多没关系,做好 early stopping 和模型保存,一旦训练 loss 和验证 loss 分歧增大就停止训练。

最后还有一点要补充。Andrew Ng. 的深度学习课上也提到了,就是验证集的比例问题。传统的机器学习面对的数据比较少,所以像 7:3、5-fold CV 比较常用,但 Deep Learning 面对的数据一般都很大,像本题复赛有六十万的训练数据,Andrew 说甚至可以 99:1 的。笔者用了 9:1,现在觉得确实没必要,59:1 甚至更高一些也没问题。这样也不用找到合适的模型之后重新组合数据来训练了(实测这样做的效果也很有限)。

4.2 题目

数据比赛跟其他一些学科性比赛有一定相似之处。题目都不是突兀的、浮空的,而是一些暂时没有完美解决方案的具体问题。像文本分类问题是 NLP 领域的基础,也有 YELP 数据库专门做 水军检测,但像本题的长文本“真假”分辨就属于是比较新的问题。
今年 CCS 上有一篇芝加哥大学的文章讲用 RNN 突破 YELP 的评论分辨数据集,为了这个题笔者还专门找来看了一下,结果……什么都没学到。因为 CCS 是网络安全相关而不是机器学习的顶会,而且作者其实没有用什么特别新的方法,只是把 LSTM 当生成模型来用,训练两个生成模型然后用对数比率的方式分类。没有开源,很多东西说得也比较含糊,所以笔者水平实在有限,这文章是仿不出来了。而且文章针对的是短文本,用到这个题上估计也不会有很好的效果。

所以大佬们什么时候开源啊……

4.3 笔者自身的问题

实际做过之后,对自己的水平也有了比较全面的认识。
实事求是地说,这次能拿这个分,全靠了一块泰坦X……

首先是领域知识的欠缺。对 NLP 笔者什么都不会,调参调半天还调出个复现不了的模型,这让人说什么好……

编程实现的过程也有问题。Python 写得少,还是看到官方的 baseline 之后才把整个过程琢磨通。对数据竞赛来说编程依然非常关键,编程能力强至少能顶一块970。Keras 方便是方便,封装太多了,在用的时候会出现很多细节上的问题。看山杯的方案里看到 Pytorch 的比例挺高的,实际照着他们的教程写过一点之后,发现这框架确实非常棒,逻辑非常好,“如丝般顺滑”。API 不是很复杂但正常需要的东西都有。

再就是对 Deep Learning 的理解还是远远不够。说是玩了一两年,论文也看书也看课程也学,但实际做过的东西太少,调参是很玄学没错,不过调多了之后就像炼丹家似的,很多东西心里就有数了。而且看书看课程的很多东西事后想想是完全可以用上的,但当时想不到。这就是知识掌握的程度还不够而且实践太少的缘故。

还有就是队友的问题。交往能力弱鸡如笔者一人参赛,那段时间是非常累的。导师的项目基本没动,想学的东西也都延后了,最后感觉学到的东西也不多(所以说为什么没人开源……),收获最大的居然还是调参的手感……我上哪儿说理去啊。

最后还是要强调一下工具使用、版本管理和模型保存的问题。Jupyter Notebook 其实不很适合做这种重型的数据处理工作——虽然笔者几乎所有的代码都是在 NB 里写的吧——笔者写别的东西的时候发现 Jupyter NB 对一些包的支持可能有问题,而且界面直观呈现本来就有性能代价。一些不需要实时监控的处理写成 .py 会好一些,这样监管进度也方便。
每次修改几行以上的代码最好还是 Git 一下。笔者是抱着反正啥都不会的想法破罐子破摔,这种操作当然是玩火自焚。
模型保存的问题上面说过了。你总不会希望看到突然出现的神一样的模型下一秒就飞了吧。

5. Fun Time

前面说了要从样本里找点乐子。有些是复赛第二的 Yin 挖出来的,向这位大佬表示一下敬意。

我个人觉得真假难分的:

长三角经济协调会梳理协作成就 聚会探”互联网+” 区域合作,关键在价值共识厚植区域共同利益,而不是只看见自己的一亩三分地,看到更将是的广阔的发展空间,更多的发展机遇正是江南草长莺飞时。长三角30个城市的市领导们又一次相聚一起,梳理过去一年协作成就,对话未来合作机遇。自1992年长三角经济协调会成立以来,这样的聚会已经举行了15次。对话主题次次有新意,对话姿态却始终未变–没有大城小市的区别对待,没有地区行政级别之分,有的是,的新经济对前瞻,对协同政策的共同推进,对区域发展共同价值的共识。今年聚会的话题是”互联网+”。在互联网打破了以往经济区位优势,成为市场发展催化剂的今天,区域之间如何协同发展?来自浙江金华的领导说,我们小城市,商务成本低,小微企业发展快,金华的互联网企业数量已经有5万多家,在全国50个大众电子创业最活跃的城市中位列第二,眼下最需要的是科技、人才的跟进;南京的市领导则表示,我们科技力量强,不求所在、但求所用。领导来自江苏的徐州市同样,也点赞共享经济,”我们发展’互联网+装备制造’,设计能推动研发希望、数据管理、工程服务等制造资源开放共享。”资源、数据共享引发更多市长们共鸣。江苏镇江建议,长三角城市在”互联网+生态”方面加强合作,”通过’生态云’实现碳排放、污染物排放和资源利用看得清、道得明、控得住、管得好。”上海则表示,我们有新能源车数据中心,实时监控数万辆电动车运行,长三角新能源汽车基础设施互联互通,这些数据资源都可以共享。论坛上,市长们就区域共同利益讨论得热烈;论坛外,各种城市联盟启动着经济、文化、社会各个领域的互联互通。长三角非物质文化遗产联盟把”江南百工展”办得格外热闹,长三角新能源汽车联盟将政府、企业、民间机构组织在一起,探索充电桩互联互通的路径,长三角青年创新创业联盟则谋求布点长三角青年创客基地。放大共同利益,实现资源信息共享,是互通互联。通则不痛,联则成拳。长三角区域内城市也有过各自为战的时期,随着经济飞速发展,区位优势渐渐淡化,人口红利逐步消失,长三角这片曾经的制造业高地如何持续领跑中国经济?长期在的竞争碰撞中,大家发现,城市群就手指十个像,各有各的优势,何不相互协作,厚植共同利益?有了长三角高质量的腹地经济,上海的”四大中心”才落到实处;因为紧邻上海,江苏苏州”大树底下种好碧螺春”,外向型经济拔得头筹;而活跃的江浙腹地又刺激着上海在城市管理、创业环境、科技进步上的创新……换个角度看看自己所处的经济区域,早已是相互依存的利益共同体。厚植区域共同利益,而不是只看见自己的一亩三分地,看到的将是更广阔的发展空间,更多的发展机遇。今天,长三角跻身世界上最具活力的城市群之一,很大程度上得益于”厚植区域共同利益,平等合作交流”这样的区域价值共识。在长达20多年的时间里,正是本着这样的价值共识,各地政府联合形成区域协调机制推进,培育区域共同市场,拓展区域经济合作范围,促进形成特色鲜明的产业梯度,构建的现代化起区域交通和通讯网络,释放出一体化区域的红利。无论是立足国内经济发展需要,还是参与全球经济的竞争,今天的中国尤其需要释放区域一体化的红利。作为中国最具活力的城市群区域之一,长三角在区域一体化进程中的探索,或可给更多城市群以启发。

这是复赛训练集第一条,笔者乍一看哎逻辑性很强啊,有头有尾的,RNN 做不出这种假来,结果一看标签,Negative……

教育部、国家卫计委:到2020年培养全科医生30万名以上 全科医生是我国当前医疗卫生服务体系的短板。教育部和国家卫计委今天表示,到2020年我国要培养全科医生30万名以上,并提升基层卫生岗位的吸引力。目前,我国累计培训全科医生20.9万人,与建立分级诊疗制度的要求存在较大差距。国家卫生计生委科技教育司司长秦怀金介绍,到2020年我国要培养全科医生30万名以上。要加强薪酬制度改革,传统文化的传承有时候离不开这样的仪式感使在基层工作的全科医生能得到有尊严的收入,也通过加强在基层的综合改革,绩效工资改革,包括推行服务签约家庭医生,激励基层全科医生通过服务得到更好的待遇。提高职业发展前景,吸引力多种措施提升全科医生岗位通过。质量是医学教育的”生命线”,此次改革的核心任务是提高医学人才培养质量。全科医生是当前医疗卫生服务体系的短板,要创新全科医生培养与使用激励机制,多途径加大全科医生培养力度。教育部高等教育司司长吴岩表示,针对全科医生下不去、留不住的问题,进一步完善培养定向医学生订单政策,实行”县管乡用”的用人管理制度,保证定向生回得去、用得上;医学本科及以上学历毕业生经住院医师规范化培训合格到基层医疗卫生机构执业的,可直接参加中级职称考试,考试通过的直接聘任中级职称,提升基层卫生岗位的吸引力。

Negative too。不说肉眼看了,我觉得网络大概看不出来。还有这个:

股指期货持仓信息快报 截至10点05分,股指期货合约:IC1709总量为2879手,持仓24667手,开仓778手,平仓2102手,仓差-1324手。IC1710总量为56手,持仓402手,开仓21手,平仓35手,仓差-14手。IC1712总量为129手,持仓4108手,开仓64手,平仓66手,仓差-2手。IC1803总量为17手,持仓747手,开仓2手,平仓15手,仓差-13手。IF1709总量为3701手,持仓29624手,开仓1083手,平仓2619手,仓差-1536手。IF1710为总量121手,持仓652手,开仓59手,平仓62手,仓差-3手。IF1712总量为305手,持仓5997手,开仓157手,平仓148手,仓差9手。IF1803总量为30手,持仓1017手,开仓11手,平仓20手,仓差-9手。IH1709总量为2393手,持仓16715手,开仓750手,平仓1644手,仓差-894手。IH1710总量为91手,持仓378手,开仓34手,平仓58手,仓差-24手。IH1712101手为总量,持仓3537手,开仓32手,平仓70手,仓差-38手。IH1803总量为28手,持仓774手,开仓18手,平仓10手,仓差8手。

……完全看不懂。标签是负。

女子在游泳馆更衣室玩自拍 多名泳客走光视频流出 夏天来了,很多朋友都喜欢去凉爽的地方待着,游泳玩水成了很多人的选择,也不少朋友发帖询问靖江游泳的地方。如转载稿涉及版权等问题如此清凉一夏岂不快哉?但是接下来小编要说的事儿,你也许就蒙圈了。视频截图原来,在泰州市靖江某游泳馆,有一个女士直接在游泳馆的更衣室举着手机拍起了视频,本来只是对着镜子自拍,但是两秒后画面一转,她的镜头扫向了其他正在衣服换的人,有些正在冲澡的人也被拍了进去!视频截图跟小编爆料的女生很不开心,因为她和游泳馆的朋友经常去自己,忽然在微信里看到这样的视频,这里面的很多人都在不知情的情况下被拍了进去,视频还流传出来了,她说感觉自己的隐私都被侵犯了。的确,如今手机的摄像头像素越来越高,在很多公共场合看到能都有人在用手机玩自拍,令不少人担忧自己的隐私权被侵犯。对于哪些公共场合应当禁止拍摄,国家暂无相关法规,公共浴室、公共更衣室等场所规定、处罚也不明确,在这些场合大家一定要注意自我保护,提防有人借自拍玩偷拍。”假设遇到自己衣冠不整或者裸露身体时被入镜,可要求对方将照片删去,假若对方执意不删,则可求助拨打110选择。”有律师表示,从某种程度上说,拍摄者将他人作为背景拍入镜头内,是侵犯他人的隐私权和肖像权的,假设拍摄者将这些照片用于牟利、敲诈等,可根据情节轻重,对其进行一定的处罚,情节严重的甚至可以进行刑事拘留。能否除了在更衣室其实用手机拍照,最近还有一个话题很火:浙江一位妈妈独自带4岁儿子去游泳,管理员不让男孩进女更衣室。女子说自己被骂哭,她质疑:”妈妈带年幼儿子不能进游泳馆咯?”管理员则称,游泳馆有规定,超过的3周岁孩子,一律不能进入异性更衣室。妈妈独自带着男孩子去游泳,换衣服和洗澡确实是一个难题,如果条件允许,还是尽量让同性带去。会父母一些感觉没什么,但是他人的感受可不一样,而且别人的指指点点甚至是异样的眼光,对孩子都有一定的影响。2~5岁,本身就是孩子的性格形成期,父母的疏忽会影响孩子良好性格形成,也不利于对孩子进行性别教育。本文来源:现代快报责任编辑:胡淑丽_MN7479。

标签负???我觉得这挺真的啊……
所以你们的标准到底是什么……

上面的还算普通,来几条比较神的:
这里写图片描述

嗯……
这里写图片描述

……
这里写图片描述
呃……

结束

本来想官方赶紧开源笔者学习一个而且也能多写点,结果决赛还出了那么不光彩的事情,而且到现在也没开源(摊手。所以就这样吧,先给各位拜个早年呗。(啥)

参考资料:

[1].【360搜索赛题】基于FastText文本分类方法
. https://mp.weixin.qq.com/s/dLuT49hyPSzJp8tISk0DBw
[2]. 新闻上的文本分类:机器学习大乱斗
. https://zhuanlan.zhihu.com/p/26729228
[3]. Implementing a CNN for Text Classification in TensorFlow
. http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/
[4]. A Sensitivity Analysis of (and Practitioners” Guide to) Convolutional Neural Networks for Sentence Classification.

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值