第一次參加阿里天池的比賽,雖然總成績沒能進前十,但是網(wǎng)頁題做的還不錯(最終排名第二,成績0.8312分)。在這里記錄一下網(wǎng)頁文本分類題的解題思路和賽后總結:
出題背景是阿里云服務器上存在著很多非法網(wǎng)頁,需要通過機器學習算法識別出網(wǎng)頁的類別,對非法網(wǎng)頁進行打擊。題目的詳細描述見賽題官方網(wǎng)址賽題二。原始數(shù)據(jù)是網(wǎng)頁靜態(tài)html代碼,但可轉換為文本分類問題。訓練集的標簽已給出,共有4個類別:
- fake_card(證件卡票):提供偽造證件、發(fā)票、證明材料等制作服務;
- gambling(賭博類):包括賭博交易、網(wǎng)絡博彩、賭博器具等;
- sexy(色情類):包括色情傳播、文圖視頻、網(wǎng)絡招嫖等;
- normal(正常類):不包含以上內(nèi)容的網(wǎng)頁。

算法整體流程圖:

關鍵步驟:
數(shù)據(jù)預處理
訓練數(shù)據(jù)中,id字段是經(jīng)過hash處理的,會出現(xiàn)id重復的情況,需要根據(jù)id對數(shù)據(jù)進行去重;另外對各類型網(wǎng)頁html文本長度的統(tǒng)計結果如下所示:

網(wǎng)頁文本長度大于200000的不到網(wǎng)頁數(shù)據(jù)總量的1%,且基本不包含風險網(wǎng)頁,所以對其進行清除,減少后續(xù)的計算量;觀察出字符長度小于200的網(wǎng)頁中大部分內(nèi)容為"404 ERROR","正在等待響應...",'"排隊中..."等內(nèi)容,對模型的訓練沒有用處,也要進行清除。代碼在下方:
--計算id的出現(xiàn)次數(shù)
select id, risk, html, count(id) over (partition by id) as count from ${t1} ;
--計算html文本的長度
select *, length(html) as html_length from ${t1} ;
--數(shù)據(jù)清理
select * from ${t1} where count = 1 and html_length > 200 and html_length < 200000 ;
網(wǎng)頁文本提取
html字段是網(wǎng)頁的源代碼,包含了結構化的html標簽,<style>、<script>等標簽中的內(nèi)容不包含語義信息,參考文獻[5]中結論,實驗中只對title、keywords、description、body標簽中的文本進行提取,另外去除文本中的特殊字符,僅保留英文、中文以及常見的標點符號。html中文本內(nèi)容的提取通過在MapReduce的map函數(shù)中調用jsoup庫實現(xiàn)(因為ODPS有沙箱隔離的限制,實驗中抽取了jsoup的核心代碼放到自定義的包中打包上傳)
package cn.edu.whu.tianchi_htmlclassify;
import cn.edu.whu.tianchi_htmlclassify.nodes.Document;
import cn.edu.whu.tianchi_htmlclassify.nodes.Element;
import cn.edu.whu.tianchi_htmlclassify.select.Elements;
import com.aliyun.odps.OdpsException;
import com.aliyun.odps.data.Record;
import com.aliyun.odps.data.TableInfo;
import com.aliyun.odps.mapred.JobClient;
import com.aliyun.odps.mapred.MapperBase;
import com.aliyun.odps.mapred.conf.JobConf;
import com.aliyun.odps.mapred.utils.InputUtils;
import com.aliyun.odps.mapred.utils.OutputUtils;
import com.aliyun.odps.mapred.utils.SchemaUtils;
import java.io.IOException;
public class HtmlExtractor {
public static void main(String[] args) throws OdpsException {
JobConf job = new JobConf();
job.setMapperClass(MapperClass.class);
job.setMapOutputKeySchema(SchemaUtils.fromString("id:string"));
job.setMapOutputValueSchema(SchemaUtils.fromString(
"title:string,keywords:string,description:string,bodytext:string"));
job.setNumReduceTasks(0);
InputUtils.addTable(TableInfo.builder().tableName(args[0]).build(), job);
OutputUtils.addTable(TableInfo.builder().tableName(args[1]).build(), job);
JobClient.runJob(job);
}
public static class MapperClass extends MapperBase {
private Record output;
@Override
public void setup(TaskContext context) throws IOException {
output = context.createOutputRecord();
}
@Override
public void map(long key, Record record, TaskContext context)
throws IOException {
//id原樣設置
output.set(0, record.get("id").toString());
String html = record.get("html").toString();
String bodyText = new String();
String title = new String();
String description = new String();
String keywords = new String();
Document doc = null;
try {
//使用Jsoup解析html
doc = Jsoup.parse(html);
//去除隱藏的html標簽
doc.select("*[style*=display:none]").remove();
//獲取title文本
Elements titles = doc.select("title");
for (Element element : titles) {
title = title + element.text();
}
//獲取keywords文本
Elements metaKey = doc.select("meta[name=keywords]");
for (Element element : metaKey) {
keywords = keywords + element.attr("content");
}
//獲取description文本
Elements metaDes = doc.select("meta[name=description]");
for (Element element : metaDes) {
description = description + element.attr("content");
}
//獲取body文本
Elements body = doc.select("body");
for (Element element : body) {
bodyText = bodyText + element.text();
}
} catch (Exception e) {
bodyText = "";
} finally {
//僅保留數(shù)字、字母、中文、常用的標點符號
output.set(1,title.replaceAll(
"[^0-9a-zA-Z\u4e00-\u9fa5.,、,。?“”]+", ""));
output.set(2,keywords.replaceAll(
"[^0-9a-zA-Z\u4e00-\u9fa5.,、,。?“”]+", ""));
output.set(3,description.replaceAll(
"[^0-9a-zA-Z\u4e00-\u9fa5.,、,。?“”]+", ""));
output.set(4,bodyText.replaceAll(
"[^0-9a-zA-Z\u4e00-\u9fa5.,、,。?“”]+", ""));
context.write(output);
}
}
}
}
文本分詞
在中文文本分類中,常用的特征單元有字、詞、字串(character n-gram)及其組合,中文中單個字的語義質量不是很好,一般不作為特征單元,詞與二字串的效果相似。在天池PAI平臺上,有基于AliWS(Alibaba Word Segmenter的簡稱)的分詞組件,若用戶指定了詞性標注或語義標注相關參數(shù),則會將分詞結果、詞性標注結果和語義標注結果一同輸出。在這里勾選詞性標注,在詞的初步過濾中會用到詞性。

詞初步過濾
- 停用詞過濾
使用 https://github.com/JNU-MINT/TextBayesClassifier 中停用詞; - 詞性過濾(僅保留動詞、名詞)
參考[7]中實驗結果,僅適用動詞、名詞作為特征詞。
詞頻統(tǒng)計
在對文章進行分詞的基礎上,按行保序輸出對應文章ID列(docId)對應文章的詞,統(tǒng)計指定文章ID列(docId)對應文章內(nèi)容(docContent)的詞頻。
使用詞頻統(tǒng)計組件分別對title、keywords、description、bodytext進行詞頻統(tǒng)計,得到文檔i中詞j出現(xiàn)在title、keywords、description、bodytext中的個數(shù)

可以對title、keywords、description、body設置不同的權重,測試了不同權重的組合,當四個標簽權重都為1時分類精度最高,詞頻權重組合的方法如下所示:
select *, (title_count * 1 + key_count * 1 + des_count * 1 + body_count) as count from ${t1}
特征詞選擇
特征詞的選擇在很多博客與論文[1][6]中都有敘述,常見的特征提取方法有文檔詞頻(Document Frequency, DF)、信息增益(Information Gain, IG)、互信息(Mutual Information, MI)、卡方統(tǒng)計(CHI)、期望交叉熵(Expected Cross Entropy)等等, 由于時間限制,我們試驗了幾種通常情況表現(xiàn)好的方法進行結合:文檔詞頻 + 信息增益 + 卡方增益,對比了幾組不同的閾值,得到了較好的分類效果。
- 文檔詞頻:在訓練文本集中對每個詞計算其文檔頻數(shù),若該詞的DF值小于某個閾值或者大于某個閾值都要進行去除,因為小于某個閾值代表了該詞“沒有代表性”,大于某個閾值代表了“沒有區(qū)分度”。
- 信息增益:信息增益是一種基于熵的評估方法,定義為某個詞為分類能夠提供的信息量, 其值為不考慮任何詞的熵與考慮該詞后的熵的差值。計算公式如下:

- 卡方統(tǒng)計:卡方統(tǒng)計體現(xiàn)特征詞與類別之間獨立性的缺乏程度,它同時考慮了特征詞存在與不存在的情況??ǚ浇y(tǒng)計值越大,獨立性越小,相關性越大。計算公式如下:

- 計算步驟:
- 統(tǒng)計訓練樣本的文檔的總數(shù)記為total_count;
- 統(tǒng)計每個類別的文檔總數(shù)分別為sexy_total_count、gamble_total_count、fake_total_count、normal_total_count;
- 計算每個詞在各類別文檔中出現(xiàn)的總次數(shù)分別記為sexy_count、gamble_count、fake_count、normal_count;
- 每個詞的文檔頻數(shù)doc_count = sexy_count + gamble_count + fake_count + normal_count;
- 根據(jù)下圖中對各類型ABCD值的定義,求出sexyA、sexyB、sexyC、sexyD、gambleA、gambleB、gambleC、gambleD、fakeA、fakeB、fakeC、fakeD、normalA、normalB、normalC、normalD;

--計算特征詞在各類型中的ABCD值
select *,
sexy_count as sexyA,
doc_count - sexy_count as sexyB,
sexy_total_count - sexy_count as sexyC,
total_count - doc_count - sexy_total_count + sexy_count as sexyD,
gamble_count as gambleA,
doc_count - gamble_count as gambleB,
gamble_total_count - gamble_count as gambleC,
total_count - doc_count - gamble_total_count + gamble_count as gambleD,
fake_count as fakeA,
doc_count - fake_count as fakeB,
fake_total_count - fake_count as fakeC,
total_count - doc_count - fake_total_count + fake_count as fakeD,
normal_count as normalA,
doc_count - normal_count as normalB,
normal_total_count - normal_count as normalC,
total_count - doc_count - normal_total_count + normal_count as normalD
from ${t1}
- 根據(jù)卡方統(tǒng)計計算出各類型CHI值,再用下式計算詞t對于整個樣本的CHI值;

--計算特征詞在各類型中的卡方統(tǒng)計
select *,
total_count * (sexyA * sexyD - sexyC * sexyB) * (sexyA * sexyD - sexyC * sexyB) /
((sexyA + sexyC) * (sexyB + sexyD) * (sexyA + sexyB) * (sexyC + sexyD))
as sexy_x2,
total_count * (gambleA * gambleD - gambleC * gambleB) * (gambleA * gambleD - gambleC * gambleB) /
((gambleA + gambleC) * (gambleB + gambleD) * (gambleA + gambleB) * (gambleC + gambleD))
as gamble_x2,
total_count * (fakeA * fakeD - fakeC * fakeB) * (fakeA * fakeD - fakeC * fakeB) /
((fakeA + fakeC) * (fakeB + fakeD) * (fakeA + fakeB) * (fakeC + fakeD))
as fake_x2,
total_count * (normalA * normalD - normalC * normalB) * (normalA * normalD - normalC * normalB) /
((normalA + normalC) * (normalB + normalD) * (normalA + normalB) * (normalC + normalD))
as normal_x2
from ${t1};
select *, GREATEST(sexy_x2, gamble_x2, fake_x2, normal_x2) as x2 from ${t1};
- 代入信息增益公式計算出word_ig值;
p(csexy) = sexy_total_count / total_count
p(t) = doc_count / total_count
p(csexy| t) = sexy_count / doc_count
p(t━) = 1 - doc_count / total_count
p(csexy| t━) = (sexy_total_count - sexy_count) / (total_count- doc_count)
select *,
- ((sexy_total_count / total_count * log(2, sexy_total_count / total_count )
+ gamble_total_count / total_count * log(2, gamble_total_count / total_count )
+ fake_total_count / total_count * log(2, fake_total_count / total_count )
+ normal_total_count / total_count * log(2, normal_total_count / total_count ) )
+ doc_count / total_count *
(sexy_count / doc_count * log(2, sexy_count / doc_count + 0.01) +
gamble_count / doc_count * log(2, gamble_count / doc_count + 0.01) +
fake_count / doc_count * log(2, fake_count / doc_count + 0.01) +
normal_count / doc_count * log(2, normal_count / doc_count + 0.01))
+ (1 - doc_count / total_count ) *
((sexy_total_count- sexy_count) / (total_count- doc_count) *
log(2, (sexy_total_count - sexy_count) / (total_count- doc_count) + 0.01) +
(gamble_total_count - gamble_count) / (total_count - doc_count) *
log(2, (gamble_total_count - gamble_count) / (total_count- doc_count) + 0.01) +
(fake_total_count - fake_count) / (total_count - doc_count) *
log(2, (fake_total_count - fake_count) / (total_count - doc_count) + 0.01) +
(normal_total_count- normal_count) / (total_count - doc_count) *
log(2, (normal_total_count - normal_count) / (total_count - doc_count) + 0.01))
as word_ig from ${t1}
- 結合doc_count,信息增益,卡方統(tǒng)計選擇特征詞(設置各自的閾值)。
特征計算
特征計算常用tfidf,它的形式為:

其中tf(ti, dj)是詞在當前文本中的局部因子,idf(ti)是只與ti相關的全局因子,在論文[2]中認為idf不能充分反映詞的重要程度,總結了一些改進的方法:

我們實驗了幾種在論文結論中表現(xiàn)較好的幾種方法:TFIDF、Probability based、tf * info gain,感到驚訝的是傳統(tǒng)的TFIDF的特征計算方法表現(xiàn)最好(當然實驗次數(shù)有限,不能充分證明)。tfidf的計算方法如下:
-- total_count 為總文檔數(shù)
-- doc_count 為特征詞在文檔中出現(xiàn)總次數(shù)
-- count 為在文檔j中特征詞i出現(xiàn)的頻數(shù)
select *, sum(tfidf_square) over ( partition by id) as tfidf_square_sum from
(select *, count * log(2, total_count / doc_count + 0.01) * count * log(2, total_count / doc_count + 0.01)
as tfidf_square from ${t1} ) c
select *, count * log(2, total_count / doc_count + 0.01) / sqrt(tfidf_square_sum) as tfidf from ${t1}
模型訓練及預測
在訓練樣本中,各類型的比值大約為50:5:3:3,需要考慮類別不平衡對模型訓練的影響[4]。常見的解決方法有隨機采樣、SMOTE算法、代價矩陣等。我們采用的是隨機采樣算法,將各類型的比值降采樣到10:5:3:3時分類效果最好。
總結
比賽之前對我們對特征工程、類別不平衡問題、特征穿越這些基本問題一無所知,都是在做題的過程中遇到了問題再去查找解決方法,加上比賽經(jīng)驗不足,過程上可謂是磕磕絆絆。給自己總結幾個經(jīng)驗和教訓:
- 經(jīng)典的算法適合入手,學習成本低,通過完善往往能夠得到不錯的結果;
- 比賽的前期要不斷的嘗試不同的思路,關注賽題官方論壇、查找相關文獻、和隊友交流思路、動手實驗要同步進行;
- 比賽后期要考慮試錯的成本,特別是最后關鍵時刻還是要采用最穩(wěn)妥的方法提高成績;
- 選擇好的隊友很關鍵,要找和自己有同樣的激情和目標的人,在這里感謝我的隊友們,一起努力走到最后。
比賽中和隊友一起進步了很多,對學習機器學習的熱情也增加了不少,感謝阿里天池給我們提供了這么好的學習平臺。
參考文獻
- Zheng Z, Wu X, Srihari R. Feature selection for text categorization on imbalanced data[J]. Acm Sigkdd Explorations Newsletter, 2004, 6(1):80-89.
- Liu Y, Han T L, Sun A. Imbalanced text classification: A term weighting approach[J]. Expert Systems with Applications, 2009, 36(1):690-701.
- Shen D, Chen Z, Yang Q, et al. Web-page classification through summarization[C]// International ACM SIGIR Conference on Research and Development in Information Retrieval. ACM, 2004:242-249.
- He H, Garcia E A. Learning from Imbalanced Data[J]. IEEE Transactions on Knowledge & Data Engineering, 2009, 21(9):1263-1284.
- Golub K, Ard? A. Importance of HTML structural elements and metadata in automated subject classification[C]// European Conference on Research and Advanced Technology for Digital Libraries. Springer-Verlag, 2005:368-378.
- 王小青. 中文文本分類特征選擇方法研究[D]. 西南大學, 2010.
- 段軍峰, 黃維通, 陸玉昌. 中文網(wǎng)頁分類研究與系統(tǒng)實現(xiàn)[J]. 計算機科學, 2007, 34(6):210-213