1. 文章概述
1.1 目的
在本篇文章中,我们将构建一个语言检测器,这是一个能够识别文本语言的简单分类器。这是一个能够识别文本是用哪种语言写的程序。想象一下,你给这个程序一段文字,它就能告诉你这是英语、法语还是其他语言。
1.2 方法
遵循频率主义者的方法,我们从零开始,几乎不假设任何关于语言的先验知识,甚至不假设单词或标记的存在。我们仅考虑构成文本的符号——Unicode字符,以及它们出现的排列。我们使用的方法叫做“朴素贝叶斯分类”。它的工作方式是通过观察文本中字母或符号的组合(我们称之为“n-gram”),因为不同的语言中这些组合的出现频率是不同的。比如说,某些字母组合在法语中很常见,但在英语中可能就很罕见。为此,我们将简单地计算字符n-gram的频率。我们的假设是,字符n-gram的分布远非随机,并且每种语言都不同:它提供了一种语言的签名。我们将通过使用(甚至构建)一个简单的朴素贝叶斯分类器来测试这一假设。
1.3 示例
如果我们有一段法文“ÉLÈVE ÈVE”,我们会注意到其中一些字母组合,比如“ÉLÈ”、“LÈV”、“ÈVE”等出现的次数。我们的程序会用这些信息来判断文本的语言。例如,法文文本"ÉLÈVE ÈVE"将包含以下的三元组计数(‘_’表示空格):
ÉLÈ: 1
LÈV: 1
ÈVE: 2
VE_: 1
E_È: 1
_ÈV: 1
三元组计数(trigram counting)是一种文本分析方法,用于统计和分析文本中每个连续的三个字符(或字母)出现的频率。这种方法把文本分成许多三个字符的组合(或称为“三元组”),然后计算每种组合在文本中出现的次数。三元组计数是自然语言处理(NLP)中常用的一种特征提取技术,尤其在语言识别、拼写检查和文本生成等任务中非常有用。
举个例子,对于文本“hello”,其三元组及其计数如下:
“hel”:1次
“ell”:1次
“llo”:1次
通过对不同语言的文本进行三元组计数,我们可以得到这些语言字符组合的频率分布。由于每种语言都有其特定的字符组合习惯,这些频率分布可以作为识别语言的依据。在朴素贝叶斯分类器中,这些三元组的频率数据会被用来预测文本所属的语言,即通过分析文本中的三元组出现频率来判断这段文本最可能是哪种语言。
1.4 朴素贝叶斯分类
朴素贝叶斯分类中的“朴素”意味着分类器将所有特征视为独立:它认为遇到ÉLÈ与遇到LÈV是独立的,这在语言中显然不是这样,但它是构建简单模型的一个方便假设。“贝叶斯”则是指贝叶斯定理:
在我们的案例中,代表我们的n-gram观察值,y是一种语言。预测给定输入三元组x时,语言是y的概率。
实际操作中,由于“朴素”属性,计算为。另外,在分类时,我们省略计算分母,因为它在各个类别中是常数(与y无关),因此不改变最终结果。我们的朴素贝叶斯分类器将简单地计算:
每种语言的单独n-gram频率(例如,ÈVE在法文、英文、印尼语等中出现的频率);
训练语料库中法文、英文等n-gram的比例:。
1.5 实现这个语言检测器的步骤
a.收集数据:首先,我们需要不同语言的文本数据。这些文本包含了我们想要识别的语言。
b.提取特征:然后,我们从这些文本中提取“n-gram”。比如,我们可以看每三个字母的组合出现了多少次。这些统计数据帮助我们理解每种语言的特点。
c.训练分类器:有了这些数据后,我们就可以训练我们的程序了。训练就是让程序学习不同语言的n-gram频率模式。
d.预测语言:一旦训练完成,我们就可以给程序任何一段文本,它会使用它学到的知识来告诉我们这段文本是什么语言。
简而言之,我们的目标是创建一个能够通过查看文本中字母组合的频率来判断语言的程序。
2. 现在让我们开始实验吧!
2.1 准备阶段
在这个练习中,你将使用Python进行编程。为了完成这个任务,你需要安装几个Python库,包括`numpy`、`scikit-learn`和(可选的)`nltk`。这些库可以通过以下命令安装:
pip install numpy
安装numpy,一个用于科学计算的库,支持大量的维度数组和矩阵运算,此外还提供了大量的数学函数库。
pip install scikit-learn
安装scikit-learn,一个用于机器学习的库。提供了许多不同的算法和工具来进行数据挖掘和数据分析。
pip install nltk
安装nltk,自然语言处理工具包,用于处理人类语言数据的库。
这个练习将使用两个语料库:
easy corpus包含10种语言,每种语言有10,000个训练句子和1,000个评估句子。
challenging corpus包含75种语言,每种语言仅有900个训练句子和100个评估句子。
第二个语料库更具挑战性,因为对于更多的目标类别(语言),训练数据要少得多。
代码可在绑定资源里下载。
2.2 初步探索
要进行这项初步探索,我们将编写一些简单的Python代码来分析语料库中的n-gram频率。这段代码将计算每种语言的唯一n-gram数量,并显示每种语言20个最常见的n-gram。n-gram的大小(数字n)将是代码中的一个可修改参数,这样你就可以用单个字符(unigrams)、两个字符(bigrams)、三个字符(trigrams)、四个字符(quadrigrams)等来试验。
首先,我将展示一个简化的代码示例,用于计算和显示n-gram的频率。这个例子将使用Python标准库来处理文本,而不是`nltk`,这样我们可以更清楚地看到底层逻辑。然后,我会展示如何使用`nltk`的`FreqDist`类来完成同样的任务。
2.2.1 使用Python标准库
我们将从基本的文本处理开始,提取n-gram并计算它们的频率。
def generate_ngrams(text, n=2):
ngrams = [text[i:i+n] for i in range(len(text)-n+1)]
return ngrams
def ngram_frequencies(texts, n=2):
from collections import Counter
ngrams_all = []
for text in texts:
ngrams = generate_ngrams(text, n)
ngrams_all.extend(ngrams)
return Counter(ngrams_all)
假设texts是一个包含多种语言文本的字典,其中键是语言名称,值是该语言的文本列表
示例: texts = {'English': ['This is a sentence.', ...], 'French': ['Ceci est une phrase.', ...]}
下面是如何使用这个函数并打印每种语言20个最常见的n-gram
n = 3 # 你可以修改这个参数来探索不同的n-gram大小
for language, text_list in texts.items():
fdist = ngram_frequencies(text_list, n)
print(f"{language} - Top 20 n-grams: {fdist.most_common(20)}")
2.2.2 使用`nltk`的`FreqDist`
如果你选择使用`nltk`,代码会更简洁,因为`FreqDist`类已经为我们处理了频率计算。
import nltk
from nltk.probability import FreqDist
def ngram_frequencies_nltk(texts, n=2):
ngrams_all = []
for text in texts:
ngrams = nltk.ngrams(text, n)
ngrams_all.extend(ngrams)
return FreqDist(ngrams_all)
# 使用nltk的FreqDist
for language, text_list in texts.items():
texts_combined = " ".join(text_list) # 将所有文本合并为一个长字符串
fdist = ngram_frequencies_nltk(texts_combined, n)
print(f"{language} - Top 20 n-grams: {fdist.most_common(20)}")
这两种方法都可以用来探索n-gram的频率分布。
n-gram数量随n的增加而指数增长,因为组合的可能性会大大增加。
比较不同语言最常见的n-gram可以揭示语言之间的相似性和差异性。
对于一些特定的n-gram,它们可能在多种语言中都很常见,而其他一些则可能是某特定语言特有的,这些差异对于构建语言检测模型特别有用。
2.2.3 最终代码
计算每种语言的唯一n-gram数量,并显示每种语言20个最常见的n-gram。
2.3 基于 Scikit 的分类
Scikit 为我们做了一切。它的 CountVectorizer 类提取 n-grams 并计算其出现次数。
出现的次数。MultinomialNB 类则根据 n-gram 计数计算似然值,用于训练、和语言类别概率进行预测。
- 查看 langdetect_scikit.py 的源代码。
- 在 "easy "语料库上运行 langdetect_scikit.py,n-gram 的大小从 1 开始依次增加。记下结果。理想的 n-gram 大小是多少?
- 注意 CountVectorizer 的参数。如果去掉重音(设置为 "unicode"),结果会怎样?(设置为 "unicode")?如果我们将文本转换为小写?
- 您还可以尝试去掉标点符号和数字:这有帮助吗?
- 在 "具有挑战性 "的语料库上也做几次测试,然后比较结果。
3. 布朗语料库的某些方面
3.1 先决条件
要在没有 root 用户的情况下安装 NLTK,您可以编写以下命令
pip install nltk --user
然后,为了加载我们需要的语料库,我们将以交互模式启动 Python,并写下
python
>>> import nltk
>>> nltk.download()
在打开的窗口中,如果没有配额问题,在 "Collections "中选择 "All",或者选择 "Book",然后点击 "Donwload"。完成操作后,按 Ctrl-D 键退出交互模式。
将使用以下模块:
import nltk
from nltk.corpus import brown
from nltk.probability import FreqDist
import re
您可以使用
brown.tagged_words()
方法获取布朗语料库中的所有单词及其 POS 标记元组。
您可以从 nltk 网页上https://www.nltk.org/book/ch02.html获取更多有关通过常规方法访问语料库的信息。
3.2 问题和答案
a) 在布朗的语料库中,哪些名词的复数比单数更常见?复数
认为复数只需加上一个 "s "即可构成)。按复数形式的出现频率和复数/单数形式的出现频率对前 20 个结果进行分类。
按复数形式的频率和复数/单数的比例对前 20 个结果进行分类。
b) 哪个单词有最多的不同标记?它们代表什么?
c) 按频率降序列出标签(前 20 个)。它们代表什么?
d) 名词前面最常见的标记是什么?它们代表什么?
4 标记器评估
4.1 基于 POS 的 POS 标记
在本节中,我们将创建标记符(默认标记符为 nltk.DefaultTagger,the n-grams tagger, nltk.NgramTagger),并使用布朗语料库中的新闻类别对它们进行训练。
我们将使用布朗语料库中的新闻类别对它们进行训练。
进行十倍交叉验证。
使用简化标签集评估相同的标记符。
4.2 基于内在属性的给定名称性别标签
在本节中,我们将使用模块
import nltk
from nltk.corpus import names
import random
import collections
姓名语料库由两个文件组成:female.txt 和 male.txt。创建一个元组列表(给定名称、性别)的随机顺序。统计上位混合名。
在上一节中,我们使用了单词和标签。在这里,我们将采用一种更通用的方法:我们将定义属性,并评估这些属性对分类任务的贡献(在我们的案例中:给定姓名的性别)。在我们的例子中:给定名字的性别)的贡献。第一个候选属性:最后一个字母。
def gender_features(word):
return {'last_letter': word[-1]}
featuresets = [(gender_features(n), g) for (n,g) in names]
train_set, test_set = featuresets[500:], featuresets[:500]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, test_set))
我们得到的就是准确率。同时计算每个性别的精确度、召回率和 F-measure(使用
使用 nltk.metrics 的同名方法,注意:要使用这些方法,您必须将给定名字的数量收集到列表中。
注意:要使用这些方法,您必须根据排名和真实性别收集列表中给定名字的数量)。我们发现了什么?
使用 classify 的 show_most_informative_features(10) 方法,我们可以找出
属性的值。
要想尝试新的属性,可以编写代码来显示所有分类错误的代码,并为每个错误显示正确的类别、错误的类别和名称。
测试其他属性及其组合。
5. 使用NLTK进行名字性别分类
在本章中,我们将使用Python的Natural Language Toolkit(NLTK)库来分析和分类名字的性别,基于名字的内在属性。我们将通过几个步骤来完成这个任务:
5.1 导入模块和数据
首先,我们需要导入所需的模块,并从NLTK的名字语料库中加载男性和女性的名字。这些名字将被存储在两个分别包含男性和女性名字的文件中。
import nltk
from nltk.corpus import names
import random
import collections
# 确保已下载名字数据集
nltk.download('names')
# 加载男性和女性的名字
male_names = names.words('male.txt')
female_names = names.words('female.txt')
5.2 创建名字和性别的元组列表
接着,我们将这些名字和对应的性别标签('male' 或 'female')组合成元组,然后将这些元组随机排序,以便后续的数据分割和分析。
# 创建名字和性别的元组列表,并随机排序
names = [(name, 'male') for name in male_names] + [(name, 'female') for name in female_names]
random.shuffle(names)
5.3 定义特征提取函数
为了分类任务,我们定义一个函数 `gender_features`,该函数基于名字的内在属性来提取特征。在这个例子中,我们使用的第一个属性是名字的最后一个字母。
# 定义特征提取函数
def gender_features(word):
return {'last_letter': word[-1]}
5.4 创建特征集
使用 `gender_features` 函数,我们为每个名字提取特征,并将这些特征与相应的性别标签组合成特征集。
# 创建特征集
featuresets = [(gender_features(n), g) for (n, g) in names]
5.5 分割数据集
我们将特征集分为训练集和测试集,用于训练和评估模型。
# 分割数据集
train_set, test_set = featuresets[500:], featuresets[:500]
5.6 训练分类器
使用训练集,我们训练一个Naive Bayes分类器。
# 训练分类器
classifier = nltk.NaiveBayesClassifier.train(train_set)
5.7 评估分类器
评估分类器在测试集上的准确性,并计算精确度(precision)、召回率(recall)和F-度量(F-measure)等性能指标。
# 计算并打印分类器的准确性
accuracy = nltk.classify.accuracy(classifier, test_set)
print(f'Accuracy: {accuracy:.2f}')
# 收集预测和真实的性别标签
test_truth = [label for (features, label) in test_set]
test_pred = [classifier.classify(features) for (features, label) in test_set]
# 计算精确度、召回率和F-度量
def get_metrics(truth, pred, label):
tp = sum(1 for t, p in zip(truth, pred) if t == label and p == label)
fp = sum(1 for t, p in zip(truth, pred) if t != label and p == label)
fn = sum(1 for t, p in zip(truth, pred) if t == label and p != label)
precision_val = tp / (tp + fp) if tp + fp > 0 else 0
recall_val = tp / (tp + fn) if tp + fn > 0 else 0
f_measure_val = 2 * precision_val * recall_val / (precision_val + recall_val) if precision_val + recall_val > 0 else 0
return precision_val, recall_val, f_measure_val
precision_male, recall_male, f_measure_male = get_metrics(test_truth, test_pred, 'male')
precision_female, recall_female, f_measure_female = get_metrics(test_truth, test_pred, 'female')
print(f"Precision (male): {precision_male:.2f}")
print(f"Recall (male): {recall_male:.2f}")
print(f"F-measure (male): {f_measure_male:.2f}")
print(f"Precision (female): {precision_female:.2f}")
print(f"Recall (female): {recall_female:.2f}")
print(f"F-measure (female): {f_measure_female:.2f}")
5.8 显示最有信息量的特征
通过 `show_most_informative_features` 方法,我们可以了解哪些属性对于性别分类最有判别力。
# 显示最有信息量的特征
classifier.show_most_informative_features(10)
5.9. 分析分类错误
编写代码来展示分类错误,为每个错误提供正确的类别、错误选择的类别和名字。
# 分析分类错误
errors = []
for (name, tag) in names[:500]:
guess = classifier.classify(gender_features(name))
if guess != tag:
errors.append((tag, guess, name))
print("Errors:")
for (tag, guess, name) in sorted(errors):
print(f"correct={tag} guess={guess} name={name}")
5.10 测试其他属性和它们的组合
探索除了名字的最后一个字母之外的其他属性,以及它们对分类任务的影响。
# 测试其他特征
def gender_features_extended(word):
return {
'last_letter': word[-1],
'first_letter': word[0],
'length': len(word),
'last_two': word[-2:]
}
# 创建扩展特征集
extended_featuresets = [(gender_features_extended(n), g) for (n, g) in names]
train_set_extended, test_set_extended = extended_featuresets[500:], extended_featuresets[:500]
# 训练扩展特征集上的分类器
classifier_extended = nltk.NaiveBayesClassifier.train(train_set_extended)
# 计算并打印扩展特征集分类器的准确性
accuracy_extended = nltk.classify.accuracy(classifier_extended, test_set_extended)
print(f'Extended Features Accuracy: {accuracy_extended:.2f}')
# 显示扩展特征集上最有信息量的特征
classifier_extended.show_most_informative_features(10)
5.11 总结
通过这些步骤,我们能够使用NLTK库构建一个简单但有效的名字性别分类器。我们从名字的最后一个字母开始提取特征,逐步扩展到包括更多的属性,并对分类器进行了训练和评估。通过这种方式,我们不仅实现了名字性别的分类,还探索了不同特征组合对分类性能的影响。这为进一步优化分类模型提供了基础,也展示了NLTK在自然语言处理任务中的强大功能。