分类是为给定的输入选择正确的类标签的任务。在基本的分类任务中,每个输入被认为是与所有其它输入隔离的,并且标签集是预先定义的。这里是分类任务的一些例子:
有监督分类:一个分类称为有监督的,如果它的建立基于训练语料的每个输入包含正确标签。有监督分类使用的框架图如1.1所示。
在4中,我们看到,男性和女性的名字有一些鲜明的特点。以a,e和i结尾的很可能是女性,而以k,o,r,s和t结尾的很可能是男性。让我们建立一个分类器更精确地模拟这些差异。下面用性别鉴定的分类来示例。
在这个例子中,我们一开始只是寻找一个给定的名称的最后一个字母。以下特征提取器函数建立一个字典,包含有关给定名称的相关信息:
"""
1. 创建一个分类器的第一步是决定输入的什么样的特征是相关的,以及如何为那些特征编码。
这里的特征是以a,e和i结尾的很可能是女性,而以k,o,r,s和t结尾的很可能是男性
"""
# 定义了一个特征提取器
def gender_features(word):
return {"last_letter": word[-1]}
gender_features("Shrek")
"""2 准备一个例子和对应类标签的列表"""
from nltk.corpus import names
labeled_names = ([(name, "male") for name in names.words("male.txt")]) + \
[(name, "female") for name in names.words("female.txt")]
import random
random.shuffle(labeled_names)
"""
3 使用特征提取器处理names数据,并划分特征集的结果链表为一个训练集和一个测试集。
训练集用于训练一个新的“朴素贝叶斯”分类器。
"""
import nltk
featuresets = [(gender_features(n), gender) for (n, gender) in labeled_names] # features = (特征,标签)
train_set, test_set = featuresets[500:], featuresets[:500] # 划分训练集和测试集
classifier = nltk.NaiveBayesClassifier.train(train_set) # 通过训练得到模型
"""4 使用模型"""
print(classifier.classify(gender_features("Neo")))
print(classifier.classify(gender_features("Trinity")))
"""5 检查分类器,确定哪些特征对于区分名字的性别是最有效的"""
classifier.show_most_informative_features(5)
print(nltk.classify.accuracy(classifier, test_set))
在处理大型语料库时,构建一个包含每一个实例的特征的单独的列表会使用大量的内存。在这些情况下,使用函数`nltk.classify.apply_features’,返回一个行为像一个列表而不会在内存存储所有特征集的对象。如下:
from nltk.classify import apply_features
train_set = apply_features(gender_features, labeled_names[500:]) # 训练集
test_set = apply_features(gender_features, labeled_names[:500]) # 测试集
特征提取通过反复试验和错误的过程建立的,由哪些信息是与问题相关的直觉指引的。它通常以“厨房水槽”的方法开始,包括你能想到的所有特征,然后检查哪些特征是实际有用的。我们在1.2中对名字性别特征采取这种做法。
def gender_features2(name):
features = {}
features["first_letter"] = name[0].lower()
features["last_letter"] = name[-1].lower()
for letter in "abcdefghijklmnopqrstuvwxyz":
features["count({})".format(letter)] = name.lower().count(letter)
features["has({})".format(letter)] = (letter in name.lower())
return features
然而,你要用于一个给定的学习算法的特征的数目是有限的——如果你提供太多的特征,那么该算法将高度依赖你的训练数据的特性,而一般化到新的例子的效果不会很好。这个问题被称为过拟合,当运作在小训练集上时尤其会有问题。
# 考虑太多特征导致这个系统的精度比只考虑每个名字最后一个字母的分类器的精度低约1%
featuresets = [(gender_features2(n), gender) for (n, gender) in labeled_names]
train_set, test_set = featuresets[500:], featuresets[:500]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, test_set)) # 书上这样说的低1%,可是这里由之前的0.762--> 0.766 ,优化了? 不过太多特征,降低处理效率
一旦初始特征集被选定,完善特征集的一个非常有成效的方法是错误分析。
"""首先,我们选择一个开发集,包含用于创建模型的语料数据。然后将这种开发集分为训练集和开发测试集。"""
train_names = labeled_names[1500:] # 训练集用于训练模型
devtest_names = labeled_names[500:1500] # 开发测试集用于进行错误分析
test_names = labeled_names[:500] # 测试集是用于测试
"""已经将语料分为适当的数据集,我们使用训练集训练一个模型,然后在开发测试集上运行"""
train_set = [(gender_features(n), gender) for (n, gender) in train_names]
devtest_set = [(gender_features(n), gender) for (n, gender) in devtest_names]
test_set = [(gender_features(n), gender) for (n, gender) in test_names]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, devtest_set))
"""使用开发测试集,我们可以生成一个分类器预测名字性别时的错误列表"""
errors = []
for (name, tag) in devtest_names:
guess = classifier.classify(gender_features(name))
if guess != tag:
errors.append((tag, guess, name))
"""
然后,可以检查个别错误案例,在那里该模型预测了错误的标签,
尝试确定什么额外信息将使其能够作出正确的决定(或者现有的哪部分信息导致其做出错误的决定)。
然后可以相应的调整特征集
"""
count = 0
for (tag, guess, name) in sorted(errors):
print("correct={:<8} guess={:<8s} name={:<30}".format(tag, guess, name))
count += 1
if count >10:
break
明确指出一些多个字母的后缀可以指示名字性别。例如,yn结尾的名字显示以女性为主,尽管事实上,n结尾的名字往往是男性;以ch结尾的名字通常是男性,尽管以h结尾的名字倾向于是女性。因此,调整我们的特征提取器包括两个字母后缀的特征。
def gender_features(word):
return {"suffix1": word[-1:],
"suffix2": word[-2:]}
"""使用新的特征提取器重建分类器,提高1个百分点,0.763 --> 0.773"""
train_set = [(gender_features(n), gender) for (n, gender) in train_names]
devtest_set = [(gender_features(n), gender) for (n, gender) in devtest_names]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, devtest_set))
使用这些语料库,我们可以建立分类器,自动给新文档添加适当的类别标签。
首先,我们构造一个标记了相应类别的文档清单。对于这个例子,我们选择电影评论语料库,将每个评论归类为正面或负面。
"构造一个标记了相应类别的文档清单"
from nltk.corpus import movie_reviews
documents = [(list(movie_reviews.words(fileid)), category)
for category in movie_reviews.categories()
for fileid in movie_reviews.fileids(category)]
random.shuffle(documents)
接下来,我们为文档定义一个特征提取器,这样分类器就会知道哪些方面的数据应注意(1.4)。对于文档主题识别,我们可以为每个词定义一个特性表示该文档是否包含这个词。为了限制分类器需要处理的特征的数目,我们一开始构建一个整个语料库中前2000个最频繁词的列表。然后,定义一个特征提取器,简单地检查这些词是否在一个给定的文档中。
"构建一个整个语料库中前2000个最频繁词的列表"
all_words = nltk.FreqDist(w.lower() for w in movie_reviews.words())
word_features = list(all_words)[:2000]
def document_features(document):
document_words = set(document)
features = {}
for word in word_features:
features["contains({})".format(word)] = (word in document_words)
return features
已经定义了我们的特征提取器,可以用它来训练一个分类器,为新的电影评论加标签。
"""训练分类器"""
featuresets = [(document_features(d), c) for (d,c) in documents]
train_set, test_set = featuresets[100:], featuresets[:100]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, test_set)) # 0.83,还好?
训练一个分类器来算出哪个后缀最有信息量
from nltk.corpus import brown
suffix_fdist = nltk.FreqDist()
# 获得后缀的计数的字典
for word in brown.words():
word = word.lower()
suffix_fdist[word[-1:]] += 1
suffix_fdist[word[-2:]] += 1
suffix_fdist[word[-3:]] += 1
common_suffixes = [suffix for (suffix, count) in suffix_fdist.most_common(100)]
print(common_suffixes)
"""定义一个特征提取器函数,检查给定的单词的这些后缀"""
def pos_features(word):
features = {}
for suffix in common_suffixes:
features["endswith({})".format(suffix)] = word.lower().endswith(suffix)
return features
特征提取函数的行为就像有色眼镜一样,强调我们的数据中的某些属性(颜色),并使其无法看到其他属性。分类器在决定如何标记输入时,将完全依赖它们强调的属性。在这种情况下,分类器将只基于一个给定的词拥有(如果有)哪个常见后缀的信息来做决定。
"""用它来训练一个新的“决策树”标记分类器"""
tagged_words = brown.tagged_words(categories='news')
featuresets = [(pos_features(n), g) for (n,g) in tagged_words]
size = int(len(featuresets) * 0.1)
train_set, test_set = featuresets[size:], featuresets[:size]
classifier = nltk.DecisionTreeClassifier.train(train_set)
nltk.classify.accuracy(classifier, test_set)
classifier.classify(pos_features("cats"))
# 决策树模型的一个很好的性质是它们往往很容易解释——我们甚至可以指示NLTK将它们以伪代码形式输出:
print(classifier.pseudocode(depth = 4))
"""
1. 可以看到分类器一开始检查一个词是否以逗号结尾——如果是,它会得到一个特别的标记","。
2. 接下来,分类器检查词是否以"the"尾,这种情况它几乎肯定是一个限定词。这个“后缀”被决策树早早使用是因为词"the"太常见。
3. 分类器继续检查词是否以"s"结尾。如果是,那么它极有可能得到动词标记VBZ(除非它是这个词"is",它有特殊标记BEZ).
4. 如果不是,那么它往往是名词(除非它是标点符号“.”)。
5. 实际的分类器包含这里显示的if-then语句下面进一步的嵌套,参数depth=4 只显示决策树的顶端部分。
"""
通过增加特征提取函数,我们可以修改这个词性标注器来利用各种词内部的其他特征,例如词长、它所包含的音节数或者它的前缀。
然而,只要特征提取器仅仅看着目标词,我们就没法添加依赖词出现的上下文语境特征。
为了采取基于词的上下文的特征,我们必须修改以前为我们的特征提取器定义的模式。不是只传递已标注的词,我们将传递整个(未标注的)句子,以及目标词的索引。
"""使用依赖上下文的特征提取器定义一个词性标记分类器。"""
def pos_features(sentence, i):
features = {"suffix(1)": sentence[i][-1:],
"suffix(2)": sentence[i][-2:],
"suffix(3)": sentence[i][-3:]}
if i == 0:
features["prev-word"] = ""
else:
features["prev-word"] = sentence[i - 1]
return features
pos_features(brown.sents()[0],8)
tagged_sents = brown.tagged_sents(categories='news')
featuresets =[]
for tagged_sent in tagged_sents:
untagged_sent= nltk.tag.untag(tagged_sent)
for i,(word,tag) in enumerate(tagged_sent):
featuresets.append((pos_features(untagged_sent,i), tag))
size = int(len( featuresets)*0.1)
train_set,test_set = featuresets[size:],featuresets[:size]
classifier = nltk.NaiveBayesClassifier.train(train_set)
nltk.classify.accuracy(classifier,test_set) # 加入句子后,利用上下文特征提高了我们的词性标注器的准确性。
一种序列分类器策略,称为连续分类或贪婪序列分类,是为第一个输入找到最有可能的类标签,然后使用这个问题的答案帮助找到下一个输入的最佳的标签。这个过程可以不断重复直到所有的输入都被贴上标签。
在下面例子中演示了这一策略。
# sentence:上下文
# i:词语后缀
# history:目标词左侧的词的标记
def pos_features(sentence, i, history):
features = {"suffix(1)": sentence[i][-1:],
"suffix(2)": sentence[i][-2:],
"suffix(3)": sentence[i][-3:]}
if i == 0:
features["prev-word"] = ""
features["prev-tag"] = ""
else:
features["prev-word"] = sentence[i - 1]
features["prev-tag"] = history[i - 1]
return features
class ConsecutivePosTagger(nltk.TaggerI):
def __init__(self, train_sents):
train_set = []
for tagged_sent in train_sents:
untagged_sent = nltk.tag.untag(tagged_sent)
history = []
for i, (word, tag) in enumerate(tagged_sent):
featureset = pos_features(untagged_sent, i, history)
train_set.append( (featureset, tag) )
history.append(tag)
self.classifier = nltk.NaiveBayesClassifier.train(train_set)
def tag(self, sentence):
history = []
for i, word in enumerate(sentence):
featureset = pos_features(sentence, i, history)
tag = self.classifier.classify(featureset)
history.append(tag)
return zip(sentence, history)
tagged_sents = brown.tagged_sents(categories='news')
size = int(len(tagged_sents) * 0.1)
train_sents, test_sents = tagged_sents[size:], tagged_sents[:size]
tagger = ConsecutivePosTagger(train_sents)
print (tagger.evaluate(test_sents))
转型联合分类的工作原理是为输入的标签创建一个初始值,然后反复提炼那个值,尝试修复相关输入之间的不一致。
另一种方案是为词性标记所有可能的序列打分,选择总得分最高的序列。
句子分割可以看作是一个标点符号的分类任务:每当我们遇到一个可能会结束一个句子的符号,如句号或问号,我们必须决定它是否终止了当前句子。
"""1 获得一些已被分割成句子的数据,将它转换成一种适合提取特征的形式"""
sents = nltk.corpus.treebank_raw.sents()
tokens = []
boundaries = set()
offset = 0
for sent in sents:
tokens.extend(sent) # tokens是单独句子标识符的合并列表
offset += len(sent)
boundaries.add(offset - 1) # boundaries是一个包含所有句子边界词符索引的集合
"""2 需要指定用于决定标点是否表示句子边界的数据特征"""
def punct_features(tokens, i):
return {'next-word-capitalized': tokens[i+1][0].isupper(),
'prev-word': tokens[i-1].lower(),
'punct': tokens[i],
'prev-word-is-one-char': len(tokens[i-1]) == 1}
"""
3 基于这一特征提取器,我们可以通过选择所有的标点符号创建一个加标签的特征集的列表,然后标注它们是否是边界标识符
"""
featuresets = [(punct_features(tokens, i), (i in boundaries))
for i in range(1, len(tokens)-1)
if tokens[i] in '.?!']
"""
4 使用这些特征集,我们可以训练和评估一个标点符号分类器
"""
size = int(len(featuresets) * 0.1)
train_set, test_set = featuresets[size:], featuresets[:size]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, test_set)) # 0.936026936026936
使用这种分类器进行断句,我们只需检查每个标点符号,看它是否是作为一个边界标识符;在边界标识符处分割词列表。原理实现如下:
def segment_sentences(words):
start = 0
sents = []
for i, word in enumerate(words):
if word in '.?!' and classifier.classify(punct_features(words, i)) == True:
sents.append(words[start:i+1])
start = i+1
if start < len(words):
sents.append(words[start:])
return sents
NPS聊天语料库,在1中的展示过,包括超过10,000个来自即时消息会话的帖子。这些帖子都已经被贴上15 种对话行为类型中的一种标签,例如“陈述”,“情感”,“yn问题”和“Continuer”。
"""
1 第一步是提取基本的消息数据。我们将调用xml_posts()来得到一个数据结构,表示每个帖子的XML注释:
"""
posts = nltk.corpus.nps_chat.xml_posts()[:10000]
"""
2 第二步 我们将定义一个简单的特征提取器,检查帖子包含什么词
"""
def dialogue_act_features(post):
features = {}
for word in nltk.word_tokenize(post):
features['contains({})'.format(word.lower())] = True
return features
"""
3 最后,我们通过为每个帖子提取特征(使用post.get(‘class’)获得一个帖子的对话行为类型)构造训练和测试数据,并创建一个新的分类器
"""
featuresets = [(dialogue_act_features(post.text), post.get('class'))
for post in posts]
size = int(len(featuresets) * 0.1)
train_set, test_set = featuresets[size:], featuresets[:size]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print(nltk.classify.accuracy(classifier, test_set)) # 0.667
识别方法:
1. 在我们的RTE 特征探测器(2.2)中,我们让词(即词类型)作为信息的代理,我们的**特征计数词重叠的程度和假设中有而文本中没有的词的程度**(由hyp_extra()方法获取)。
2. 不是所有的词都是同样重要的——命名实体,如人、组织和地方的名称,可能会更为重要,这促使我们分别为words和nes(命名实体)提取不同的信息。3. 此外,一些高频虚词作为“停用词”被过滤掉。
def rte_features(rtepair):
extractor = nltk.RTEFeatureExtractor(rtepair)
features = {}
features['word_overlap'] = len(extractor.overlap('word'))
features['word_hyp_extra'] = len(extractor.hyp_extra('word'))
features['ne_overlap'] = len(extractor.overlap('ne'))
features['ne_hyp_extra'] = len(extractor.hyp_extra('ne'))
return features
# 说明这些特征的内容,我们检查前面显示的文本/假设对34的一些属性
rtepair = nltk.corpus.rte.pairs(['rte3_dev.xml'])[33]
extractor = nltk.RTEFeatureExtractor(rtepair)
print(extractor.text_words)
print(extractor.hyp_words)
print(extractor.overlap('word'))
print(extractor.overlap('ne'))
print(extractor.hyp_extra('word'))
# 这些特征表明假设中所有重要的词都包含在文本中,因此有一些证据支持标记这个为True。
# nltk.classify.rte_classify模块使用这些方法在合并的RTE测试数据上取得了刚刚超过58%的准确率。这个数字并不是很令人印象深刻的,还需要大量的努力,更多的语言学处理,才能达到更好的结果。
安装其他软件包
# 从一个反映单一的文体(如新闻)的数据源随机分配句子,创建训练集和测试集
import random
from nltk.corpus import brown
tagged_sents = list(brown.tagged_sents(categories='news'))
random.shuffle(tagged_sents)
size = int(len(tagged_sents) * 0.1)
train_set, test_set = tagged_sents[size:], tagged_sents[:size]
缺陷:
# 稍好的做法是确保训练集和测试集来自不同的文件
file_ids = brown.fileids(categories='news')
size = int(len(file_ids) * 0.1)
train_set = brown.tagged_sents(file_ids[size:])
test_set = brown.tagged_sents(file_ids[:size])
# 要执行更令人信服的评估,可以从与训练集中文档联系更少的文档中获取测试集
train_set = brown.tagged_sents(categories='news')
test_set = brown.tagged_sents(categories='fiction')
准确度:测量测试集上分类器正确标注的输入的比例
posts = nltk.corpus.nps_chat.xml_posts()[:10000]
def dialogue_act_features(post):
features = {}
for word in nltk.word_tokenize(post):
features['contains({})'.format(word.lower())] = True
return features
featuresets = [(dialogue_act_features(post.text), post.get('class'))
for post in posts]
size = int(len(featuresets) * 0.1)
train_set, test_set = featuresets[size:], featuresets[:size]
classifier = nltk.NaiveBayesClassifier.train(train_set)
print('Accuracy: {:4.2f}'.format(nltk.classify.accuracy(classifier, test_set)))
真阳性是相关项目中我们正确识别为相关的。
真阴性是不相关项目中我们正确识别为不相关的。
假阳性(或I 型错误)是不相关项目中我们错误识别为相关的。
假阴性(或II型错误)是相关项目中我们错误识别为不相关的。
对角线项目(即cells |ii|)表示正确预测的标签,非对角线项目表示错误,如下例
brown_tagged_sents = brown.tagged_sents(categories='news')
brown_sents = brown.sents(categories='news')
size = int(len(brown_tagged_sents) * 0.9)
train_sents = brown_tagged_sents[:size]
test_sents = brown_tagged_sents[size:]
t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(train_sents, backoff=t0)
t2 = nltk.BigramTagger(train_sents, backoff=t1)
这个混淆矩阵显示常见的错误,包括NN替换为了JJ(1.6%的词),NN替换为了NNS(1.5%的词)。注意点(.)表示值为0 的单元格,对角线项目——对应正确的分类——用尖括号标记。
交叉验证:在不同的测试集上执行多个评估,然后组合这些评估的得分。
python机器学习 | 决策树算法介绍及实现
python机器学习 | 朴素贝叶斯算法介绍及实现
生成式模型与条件式模型之间的差别类似与一张地形图和一张地平线的图片之间的区别。 虽然地形图可用于回答问题的更广泛,制作一张精确的地形图也明显比制作一张精确的地平线图片更加困难。