本文内容翻译自 http://nlp.stanford.edu/software/tmt/tmt-0.4/ 目前斯坦福大学的TMT(Topic Modeling Toolbox)已经更新至0.4.0版本。笔者最近在研究这个小工具,特此做一些笔记,在使用中遇到一些问题也进行说明。 这个小工具是scala写的,但是本机不需要安装scala,需要安装jre1.5以上版本,笔者使用的是1.7.79版本。 按照官网的说明,运行一个最简单的例子: 首先下载tmt-0.4.0.jar,example-0-test.scala和pubmed-oa-subset.csv,这三个文件要放在同一个文件夹里。第一个文件是一个程序的jar包,也就是程序的主体;第二个文件是程序所运行的脚本,定义了处理的数据和显示结果等;第三个文件就是进行操作的数据了。 以下就是主界面,点击File——>open script...打开脚本,比如之前下载的example-0-test.scala
这些按钮中,Edit script可以修改脚本,比如修改处理的数据集。修改完成之后,按run就可以了。
需要注意的是,在官网下载的示例数据集pubmed-oa-subset.csv中,存在类似中文的乱码,这些乱码会导致报错,所以要把这些乱码删除。至于网上说的要把字符集修改成utf-8,笔者没有遇到这样的问题。
本例中使用的代码是example-1-dataset.scala文件
从一个CSV文件提取并且准备文本的过程可以被看做一个流水线:一个CSV文件经过一系列过程最终成为可以用来训练模型的结果。这里就是pubmed-oa-subset.csv数据文件的案例:
01.
val
source
=
CSVFile(
"pubmed-oa-subset.csv"
) ~> IDColumn(
1
);
02.
03.
val
tokenizer
=
{
04.
SimpleEnglishTokenizer() ~>
// tokenize on space and punctuation
05.
CaseFolder() ~>
// lowercase everything
06.
WordsAndNumbersOnlyFilter() ~>
// ignore non-words and non-numbers
07.
MinimumLengthFilter(
3
)
// take terms with >=3 characters
08.
}
09.
10.
val
text
=
{
11.
source ~>
// read from the source file
12.
Column(
4
) ~>
// select column containing text
13.
TokenizeWith(tokenizer) ~>
// tokenize with tokenizer above
14.
TermCounter() ~>
// collect counts (needed below)
15.
TermMinimumDocumentCountFilter(
4
) ~>
// filter terms in <4 docs
16.
TermDynamicStopListFilter(
30
) ~>
// filter out 30 most common terms
17.
DocumentMinimumLengthFilter(
5
)
// take only docs with >=5 terms
18.
}
输入的数据文件(在代码变量中)是一个指向你先前下载的CSV文件的指针,随后我们将经过一系列的变形、过滤或者其他与数据交互的操作。第一行代码中,制定了TMT使用第一列(column 1)的值作为记录ID,这对文件中每一条记录都是独一无二的标志。如果你的sheet中的记录ID不在第一列,就把上文代码第一行中的1改成你自己的列数。如果你的sheet没有记录ID这一列,你可以删掉“~> IDColumn(1)”,TMT会用文件的行号作为记录ID。 如果你的CSV文件第一行包含了列名,你可以删除第一行代码,改用Drop步骤:
1.
val
source
=
CSVFile(
"your-csv-file.csv"
) ~> IDColumn(yourIdColumn) ~> Drop(
1
);
第一步是定义分词器(tokenizer),以将数据集中包含文本的单元转化成话题模型分析的term。从第三行到第七行定义的分词器,制定了一系列的将一个字符串转化成一系列字符串的变形操作。
笔者注:每两个步骤之间需要有~>符号,最后一个不需要
首先,我们用SimpleEnglishTokenizer()去除单词结尾的标点符号,然后用空白符(tab、空格、回车等)将输入文本分解。如果你的文件已经进行过清洗了,你也可以用 WhitespaceTokenizer()。或者,你可以用RegexSplitTokenizer("your-regex-pattern"),通过正则表达式定制你自己的分词器。 CaseFolder随后被用来将每个单词变成小写,这样“The”、“tHE”、“THE”都变成了“the”。CaseFolder通过把所有字符变成小写形式,减少了单词的不同形式。 下面,使用WordsAndNumbersOnlyFilter(),纯标点、非单词非数字的字符会从产生的分词后的文档列表中删除。 最后,使用MinimumLengthFilter()将短于3个字符的term去除 作为可选功能,token可以用 PorterStemmer()在MinimumLengthFilter()之前提取词干。提取词干在信息检索中是一种常用的技术,将比如多元词转化成简单的常用term(“books”和“book”都映射成“book”)。但是,提取词干并不总对话题建模有益,因为有时提取词干会把一些term合并在一起,但是他们最好还是分开,而且同一个单词的变形会变成同一个话题。 如果你想要去除标准的英语停用词(stop word),可以在分词器的最后一步用StopWordFilter("en")(笔者注:如果使用了这一步,那么这些很常用的停用词都会被过滤掉,在下面的步骤中,被过滤掉的前30个常用单词就很可能是有用单词了)
定义好分词器之后,我们就可以用它从CSV文件中合适的列中提取文本了。如果你的文本数据存在于一列中(这里是第四列):
1.
source ~> Column(
4
) ~> TokenizeWith(tokenizer)
以上的代码会加载CSV文件中的第四列文本
如果你的文本不止存在于一列中:
1.
source ~> Columns(
3
,
4
) ~> Join(
" "
) ~> TokenizeWith(tokenizer)
以上的代码会选择第三和第四列,然后把他们的内容用一个空格连在一起。
话题建模对于有意义单词的模式提取(extracting patterns)非常有用,但是在决定什么单词是有意义时并不一定奏效。通常,使用常见的单词比如“the”,并不代表着文档之间的相似性。为了在有意义的单词中提取模式,我们使用一系列的标准启发式算法:
1.
... ~>
2.
TermCounter ~>
3.
TermMinimumDocumentCountFilter(
4
) ~>
4.
TermDynamicStopListFilter(
30
) ~>
5.
...
上面的代码去除了在少于四篇文档中出现的term(因为很少见的单词几乎不对文档相似度做出贡献),还有在文本库中最常见的30个单词(因为太普遍的单词同样对文档相似度不做出贡献,他们通常被定义为停用词)。当你在处理很大或者很小(少于几千单词的文档)时,这些值可能需要更新。 如果你有一个你想要出去的停用词的详细列表,你可以像这样额外增加一个过程: TermStopListFilter(List("positively","scrumptious")). 这里,在引号的List里添加你需要过滤的单词。记住,TermStopListFilter 运行在文档被分词之后,所以你提供的List要和你的分词器输出保持一致,就是说,如果你的分词器包括了CaseFolder和PorterStemmer,过滤的单词必须也要是小写的和词干。 TermCounter步骤首先必须计算下一步骤需要的一些统计。这些数据存储在元数据中,使得任何下游步骤可以使用这些数据。这些步骤也会在硬盘上CSV文件的同一个文件夹下产生缓存文件,以保存文档数据。文件名会以CSV文件的名称开头,并且会包含流水线的标记"term-counts.cache"。
数据集中的一些文档可能会丢失或者是空的(一些单词可能在最后一步被过滤掉)。可以通过使用DocumentMinimumLengthFilter(length) 在训练中舍弃一些文档,去除短于特定长度的文档。
运行example1 (example-1-dataset.scala)。这个程序会首先加载数据流水线,然后打印加载数据集的信息,包括数据集的标志和文本库中的30个停用词。(注意在PubMed,因为“gene”被广泛使用所以被过滤掉了)
这个例子展示了如何用你上面准备的数据集进行LDA训练。 这个例子的代码在example-2-Ida-learn。scala里。
01.
val
source
=
CSVFile(
"pubmed-oa-subset.csv"
) ~> IDColumn(
1
);
02.
03.
val
tokenizer
=
{
04.
SimpleEnglishTokenizer() ~>
// tokenize on space and punctuation
05.
CaseFolder() ~>
// lowercase everything
06.
WordsAndNumbersOnlyFilter() ~>
// ignore non-words and non-numbers
07.
MinimumLengthFilter(
3
)
// take terms with >=3 characters
08.
}
09.
10.
val
text
=
{
11.
source ~>
// read from the source file
12.
Column(
4
) ~>
// select column containing text
13.
TokenizeWith(tokenizer) ~>
// tokenize with tokenizer above
14.
TermCounter() ~>
// collect counts (needed below)
15.
TermMinimumDocumentCountFilter(
4
) ~>
// filter terms in <4 docs
16.
TermDynamicStopListFilter(
30
) ~>
// filter out 30 most common terms
17.
DocumentMinimumLengthFilter(
5
)
// take only docs with >=5 terms
18.
}
1.
// turn the text into a dataset ready to be used with LDA
2.
val
dataset
=
LDADataset(text);
3.
4.
// define the model parameters
5.
val
params
=
LDAModelParams(numTopics
=
30
, dataset
=
dataset);
这里你可以指定一定数量想要学习的topic。你也指定可以指定LDA模型使用的Dirichlet term和 topic smoothing参数,这些参数在第五行作为LDAModelParams额外的参数提供给构造函数。在默认情况下,第五行等价于已经设定了termSmoothing=SymmetricDirichletParams(.1)
和topicSmoothing=SymmetricDirichletParams(.1)
从0.3版本起,本工具开始支持大多数模型上的多种形式的学习和推理,包括默认支持的多线程训练和多核机器上的推理。特别的,这个模型可以使用collapsed Gibbs sampler [T. L. Griffiths and M. Steyvers. 2004. Finding scientific topics. PNAS, 1:5228–35]或者collapsed variational Bayes approximation to the LDA objective [Asuncion, A., Welling, M., Smyth, P., & Teh, Y. W. (2009)). On Smoothing and Inference for Topic Models. UAI 2009]。
01.
// Name of the output model folder to generate
02.
val
modelPath
=
file(
"lda-"
+dataset.signature+
"-"
+params.signature);
03.
04.
// Trains the model: the model (and intermediate models) are written to the
05.
// output folder. If a partially trained model with the same dataset and
06.
// parameters exists in that folder, training will be resumed.
07.
TrainCVB
0
LDA(params, dataset, output
=
modelPath, maxIterations
=
1000
);
08.
09.
// To use the Gibbs sampler for inference, instead use
10.
// TrainGibbsLDA(params, dataset, output=modelPath, maxIterations=1500);
该模型会在训练时产生状态信息,并且会把产生的模型写入当前目录的一个文件夹,在这个例子里名称为"lda-59ea15c7-30-75faccf7"。注意,默认情况下,使用CVB0LDA进行训练会使用本地所有可用的内核,而且因为它的收敛速率很快,CVB0LDA比GibbsLDA迭代次数更少,然而GibbsLDA在训练时需要更少的内存。
lda-59ea15c7-30-75faccf7
,包含了分析这个学习过程和把模型从磁盘加载回去所需要的一切。
description.txt | A description of the model saved in this folder. |
document-topic-distributions.csv | A csv file containing the per-document topic distribution for each document in the dataset. |
[Snapshot]: 00000 - 01000 | Snapshots of the model during training. |
[Snapshot]/params.txt | Model parameters used during training. |
[Snapshot]/tokenizer.txt | Tokenizer used to tokenize text for use with this model. |
[Snapshot]/summary.txt | Human readable summary of the topic model, with top-20 terms per topic and how many words instances of each have occurred. |
[Snapshot]/log-probability-estimate.txt | Estimate of the log probability of the dataset at this iteration. |
[Snapshot]/term-index.txt | Mapping from terms in the corpus to ID numbers (by line offset). |
[Snapshot]/topic-term-distributions.csv.gz | For each topic, the probability of each term in that topic. |
一种简单的判断模型的训练是否已经收敛的办法,是看计数文件夹oflog-probability-estimate.txt
.的值。这个文件包含了模型在训练时对数据概率估计的非正式估计。这些数字趋向于形成逐步向下但不会完全停止改变的曲线。如果这些数字看起来还没有稳定下来,你可能会会需要设定更高的迭代次数。
在训练中,这个工具在产生的模型文件夹中的 document-topic-distributions.csv中记录了每个训练文档的话题分布。模型训练之后,它可以用来分析另外一个可能更大的文本,这个过程称作推理。这个教程展示了如何在一个新的数据集中用已经存在的话题模型中进行推理。 这个例子的代码在example-3-Ida-infer.scala中
1.
// the path of the model to load
2.
val
modelPath
=
file(
"lda-59ea15c7-30-75faccf7"
);
3.
4.
println(
"Loading "
+modelPath);
5.
val
model
=
LoadCVB
0
LDA(modelPath);
6.
// Or, for a Gibbs model, use:
7.
// val model = LoadGibbsLDA(modelPath);
01.
// A new dataset for inference. (Here we use the same dataset
02.
// that we trained against, but this file could be something new.)
03.
val
source
=
CSVFile(
"pubmed-oa-subset.csv"
) ~> IDColumn(
1
);
04.
05.
val
text
=
{
06.
source ~>
// read from the source file
07.
Column(
4
) ~>
// select column containing text
08.
TokenizeWith(model.tokenizer.get)
// tokenize with existing model's tokenizer
09.
}
10.
11.
// Base name of output files to generate
12.
val
output
=
file(modelPath, source.meta[java.io.File].getName.replaceAll(
".csv"
,
""
));
13.
14.
// turn the text into a dataset ready to be used with LDA
15.
val
dataset
=
LDADataset(text, termIndex
=
model.termIndex);
这里我们准备了一个新的数据集,用已载入模型的原始分词器进行了分词。注意:在这个特别的例子中,我们实际上使用的是之前训练的同样的文件。在实际使用中,推理的CSV文件将是磁盘中的其他文件。
我们还创建了输出路径的文件名,下面输出的文件将出现在模型文件夹里,这些文件名将以推理的数据集名字开头。
1.
println(
"Writing document distributions to "
+output+
"-document-topic-distributions.csv"
);
2.
val
perDocTopicDistributions
=
InferCVB
0
DocumentTopicDistributions(model, dataset);
3.
CSVFile(output+
"-document-topic-distributuions.csv"
).write(perDocTopicDistributions);
4.
5.
println(
"Writing topic usage to "
+output+
"-usage.csv"
);
6.
val
usage
=
QueryTopicUsage(model, dataset, perDocTopicDistributions);
7.
CSVFile(output+
"-usage.csv"
).write(usage)
.
println(
"Estimating per-doc per-word topic distributions"
);
2.
val
perDocWordTopicDistributions
=
EstimatePerWordTopicDistributions(
3.
model, dataset, perDocTopicDistributions);
4.
5.
println(
"Writing top terms to "
+output+
"-top-terms.csv"
);
6.
val
topTerms
=
QueryTopTerms(model, dataset, perDocWordTopicDistributions, numTopTerms
=
50
);
7.
CSVFile(output+
"-top-terms.csv"
).write(topTerms);