12-2423:53 li=content.find_all("li")
每个li中又包含了三个div,分别是分类、新闻标题、发布时间。这里我们只需要前两个部分,但是还需要抓取下链接,作为去重的key:
category=item.find('div',class_='dd_lm').text.replace(r'[','').replace(r']','')
title=item.find('div',class_='dd_bt').text
href=item.find('div',class_='dd_bt').a.attrs['href']
数据存储
我比较喜欢将数据凑成字典存到Redis,方便之后使用Pandas处理。
di={'category':category,'title':title}
ifnotcli.hget('chinanews',href):
cli.hset('chinanews',href,str(di))
时间处理
上面提到页面的URL里需要用到年月日信息,我这里使用了timedelta来从当前时间往后计算,比如下面就是抓取1500天的新闻标题及分类:
today=datetime.today()
foriinrange(1,1500):
whichday=(today-timedelta(days=i)).strftime("%Y-%m%d")
fetch_oneday(whichday)
time.sleep(2) #做个有风度的爬虫
(爬虫的详细代码见文章最后的链接)
数据分析
大概一小时的收集之后,我们就有了2299879将近230万条数据:1500天的新闻标题和对应的分类。
在对数据进行预处理之前,我们必须对数据进行一个初略的分析,去掉一些噪点之类的数据。
从Redis里读入数据
cli=redis.Redis()
data=cli.hgetall('chinanews')
df=pd.DataFrame([ast.literal_eval(data[k])forkindata])
去除无用类别
categories=df.groupby('category').size()
pie=Pie("分类")
pie.add("",categories.index.tolist(),categories.values.tolist(),is_label_show=True,is_legend_show=False)
pie
一共有30个分类,并可以看到数据的分布并不均匀,我们先去掉数据量少于20000条的类别,因为数据量少对模型的准确性还是有影响的;另外对于“图片”、“视频”、“报摘”这三种分类,很明显没有区分度,只能去掉了:
categories=df.groupby('category').size()
categories=categories[categories>20000].index.tolist()
#设置map方法
filted=df['category'].map(lambdax:xincategories)
#然后应用到dataframe上
df_filted=df[filted]
df_filted=df_filted[(df_filted['category']!=u'图片')&(df_filted['category']!=u'视频')&(df_filted['category']!=u'报摘')]
现在我们还有2178902条数据,以及22种分类:
数据预处理
现在我们的数据是长这样子的:
神经网络只能处理数字,因此我们需要将category和title分别映射为数字。
category
分类的转换比较容易,可以将所有分类从1开始映射,首先生成一个分类的列表,然后生成两个字典,分别是分类名称:数字以及数字:分类名称的字典,方便映射和查找:
catagories=df_filted.groupby('category').size().index.tolist()
catagory_dict={}
int_catagory={}
fori,kinenumerate(catagories):
catagory_dict.update({k:i})
int_catagory.update({i:k})
dataframe加上一个映射的column,使用apply方法:
df_filted['c2id']=df_filted['category'].apply(lambdax:catagory_dict[x])
这样就完成了category到数字的映射,重新取title,c2id两列来准备接下来的工作:
prepared_data=df_filted[['title','c2id']]
title
新闻标题其实是个句子,我们要把句子映射为数字列表,有两种方式:
第一种是分词后再映射,另一种是直接单个字进行转换映射。
前者的优点是准确率高(假设当新闻标题里出现“勒布朗”这个词时,99%可能这是条体育新闻,而如果是单个“勒”字很显然比较难进行分类);缺点是在预测新标题时,如果出现了词库里没有的词,则无法进行预测(人名之类的最常出现这种问题,无法进行映射转换,且分词效果也不好,如果是按单个字来映射则没有这个问题)。
后者的优缺点刚好相反。
因为我们最终的目的是用作新数据的分类,所以用单字进行转换映射较好。
prepared_data['words']=prepared_data['title'].apply(lambdax:re.findall('[\x80-\xff]{3}|[\w\W]',x))
正则 [\x80-\xff]{3}|[\w\W] 中,[\x80-\xff]{3} 配置中文字符,[\w\W] 配置标点符号空格等其他所有字符。
转换后的结果:
字库
生成字的映射字典:
all_words=[]
forwinprepared_data['words']:
all_words.extend(w)
word_dict=pd.DataFrame(pd.Series(all_words).value_counts())
word_dict['id']=list(range(1,len(word_dict)+1))
我们得到了一个6790个“字”(包括标点符号和空格等)的字典,基本上包括了所有常用字,对应的id则为不同的数字。
字映射为数字
新加一个w2v的列存放转换后的数字队列(执行比较久):
prepared_data['w2v']=prepared_data['words'].apply(lambdax:list(word_dict['id'][x]))
然后补全或者截断为固定长度为25的队列(新闻标题一般不会超过25个字):
maxlen=25
prepared_data['w2v']=list(sequence.pad_sequences(prepared_data['w2v'],maxlen=maxlen))
最终准备好的dataframe:
现在,我们的X数据是w2v列,而标签(target)Y则是c2id列。
生成训练数据和测试数据
简单地使用sklearn.model_selection的 train_test_split 随机将所有数据以
3:1的比例分隔为训练数据和测试数据:
seed=7
X=np.array(list(prepared_data['w2v']))
Y=np.array(list(prepared_data['c2id']))
x_train,x_test,y_train,y_test=train_test_split(X,Y,test_size=0.25,random_state=seed)
这里,比较重要的是要对Y进行to_categorical处理,把Y变成one-hot的形式。
为什么要转成one-hot呢?因为分类器往往默认数据数据是连续的,并且是有序的。但是按照我们上述的表示,数字并不是有序的,而是随机分配的。使用one-hot,使得这些特征互斥,每次只有一个激活。因此数据就变成稀疏的了。
y_train=to_categorical(y_train)
y_test=to_categorical(y_test)
LSTM模型
建立模型
文本模型,通常都是使用RNN模型,我们直接参考Keras的LSTM官方例子即可:
https://github.com/keras-team/keras/blob/master/examples/imdb_lstm.py
model=Sequential()
model.add(Embedding(len(word_dict)+1,256))
model.add(LSTM(256))
model.add(Dropout(0.5))
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
与例子中的模型不同的是,我在中间加了层dropout避免过拟合;另外我们是多分类,所以最后的全连接层,输出应该是类别的个数,激活函数改为softmax,损失函数改为categorical_crossentropy。
训练
model.fit(x_train,y_train,batch_size=128,epochs=20)
因为数据较大,所以训练起来速度较慢,在使用了一块GTX1080GPU的情况下,一个轮次还要差不多10分钟左右。
10个轮次的训练之后,准确率下降变慢,停留在74%左右。
测试数据验证
model.evaluate(x=x_test,y=y_test)
结果:
测试集的准确率在69%左右。
使用新数据来测试模型
结果有点差强人意,我们用新数据测试下吧。
新数据需要用预处理中的方式处理成数字列表:
defpredict_(title):
words=re.findall('[\x80-\xff]{3}|[\w\W]',title)
w2v=[word_dict[word_dict['0']==x]['id'].values[0]forxinwords]
xn=sequence.pad_sequences([w2v],maxlen=maxlen)
predicted=model.predict_classes(xn,verbose=0)[0]
returnint_catagory[predicted]
再拉取新的新闻测试下效果:
以下是部分输出:
从上面的结果可以看出,其实部分新闻标题的分类,感觉模型判断出来的结果更准确,比如:
[社会]prediction:[I T]华为否认提前发年终奖网传消息实为销售激励计划
[社会]prediction:[房产]中国这个地方如同仙境豪华别墅每平米仅1300元
[汽车]prediction:[I T]阿里巴巴高管调整蒋凡任淘宝总裁靖捷任天猫总裁
[社会]prediction:[港澳]香港青年学子“冬聚吉林”:多层面了解国家变化
如果是将预测的前三种分类都当成正确的话:
defpredict_3(title):
words=re.findall('[\x80-\xff]{3}|[\w\W]',title)
w2v=[word_dict[word_dict['0']==x]['id'].values[0]forxinwords]
xn=sequence.pad_sequences([w2v],maxlen=maxlen)
predicted=model.predict(xn,verbose=0)[0]
predicted_sort=predicted.argsort()
li=[(int_catagory[p],predicted[p]*100)forpinpredicted_sort[-3:]]
returnli[::-1]
可以看到成功率接近90%
从这个结果来看,我们的模型还是可行的。
其他模型
GRU
model=Sequential()
model.add(Embedding(len(word_dict)+1,256))
model.add(GRU(256))
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
精确度略低于LSTM(训练集73.52%/10epochs;测试集67.83%);
但训练用时较少,每轮次480s左右。
BiLSTM+CNN
embedding_size=128
hidden_size=256
model=Sequential()
model.add(Embedding(input_dim=len(word_dict)+1,output_dim=128,input_length=25))
model.add(Bidirectional(LSTM(256,return_sequences=True)))
model.add(TimeDistributed(Dense(64)))
model.add(Activation('softplus'))
model.add(MaxPooling1D(5))
model.add(Flatten())
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
训练集精确度较LSTM略高(76.55%/10epochs),但测试集的精确度略低(68.29%),且训练用时更长,需要1080s/1epochs。
保存模型和数据
最终还是使用了LSTM模型。将数据和模型保存下来,方便下一次调用:
model.save('model.hdf5')
importpickle
defsave_obj(obj,name):
withopen(name+'.pkl','wb')asf:
pickle.dump(obj,f,pickle.HIGHEST_PROTOCOL)
save_obj(int_catagory,'int_catagory')
save_obj(catagory_dict,'catagory_dict')
word_dict.to_csv('word_dict.csv',encoding='utf8')
prepared_data.to_csv('prepared_data.csv',encoding='utf8')
应用
比如类似某云平台的这种API服务:
使用Flask搭建一个API服务,用户Post一个新闻标题过来,返回预测的前三种可能分类:
➜ ~curl-H"Content-Type:application/json"-XPOST-d'{"title":"彭文生:房价下降才能促进宏观杠杆率的可持续下"}'http://192.168.15.24:5000/classify
{
"result":{
"1.category":"房产",
"1.possibility":"0.650669",
"2.category":"财经",
"2.possibility":"0.285769",
"3.category":"金融",
"3.possibility":"0.0191255"
}
}
➜ ~curl-H"Content-Type:application/json"-XPOST-d'{"title":"潘粤明后台自拍扮相温文尔雅表情搞怪反差萌"}'http://192.168.15.24:5000/classify
{
"result":{
"1.category":"娱乐",
"1.possibility":"0.845201",
"2.category":"台湾",
"2.possibility":"0.108812",
"3.category":"港澳",
"3.possibility":"0.0148304"
}
}
还不赖吧,现在,你可以炒掉那些只会给新闻分类的网站编辑了。
代码及Jupyter笔记:https://github.com/jackhuntcn/news_category_classify
https://mp.weixin.qq.com/s/qR-d9Zay-7NJZgmYYlwn0A?utm_source=tuicool&utm_medium=referral
我们在浏览新闻的时候,通常会看到新闻网站对每个新闻都进行了分类:
新闻分类的应用相当广泛。对于网站来说,可以根据你看得较多的新闻类别给你推荐新闻;对于用户来说,则是可以忽略掉不感兴趣的分类,提高了浏览体验。
比如我抓取了近一个月,网易新闻APP向我推荐的13.7万条新闻,以下的新闻类别图彻底地暴露了本人是个喜欢看体育和花边娱乐新闻的俗人:
那么各大新闻网站的新闻分类是如何对新闻进行分类的呢?据了解,有可能是网站编辑人工进行分类,但目前更有可能是通过各种高级的算法和AI来进行自动分类。
本文使用Python和Keras,展示了如何从收集数据开始,到数据分析、预处理,再到使用深度学习/神经网络创建一个准确率达到人类水准的新闻分类器。
虽然这是个比较啰里吧嗦的教程,但是这里的“从零开始”,是假设你已熟悉了Python基础语法的基础上的。
选择要爬取的数据源
我们知道,在监督学习中,数据的预处理往往花费的时间要比真正训练模型的时候还要多。所以找到一个规整易于爬取的数据源是相当重要的。
来看下我们的需求,即“给出一个新闻标题,返回该新闻的分类”,那么我们收集的每一条数据中都必须有新闻标题和分类。
在各个提供了历史新闻的新闻网站中,中国新闻网(http://www.chinanews.com)的滚动新闻页面应该是最容易爬取的了:
网页链接,如:http://www.chinanews.com/scroll-news/2017/1224/news.shtml 可以直接指定某天的新闻,并且该页面直接包含了当天所有的新闻标题和对应的分类。
编写数据爬虫
还是以上面的链接 http://www.chinanews.com/scroll-news/2017/1224/news.shtml 为例,在页面上右键点击“显示网页源代码”,很容易可以找到我们所需要爬取的新闻标题和分类: