# 参考
https://www.cntofu.com/book/170/docs/58.md(中文文档)
https://scikit-learn.org/stable/user_guide.html(官方文档)
https://runwei.blog.csdn.net/article/details/107589938?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&utm_relevant_index=1
scikit-learn提供了从文本内容中提取数字特征的最常见方法:
在该方案中,特征和样本定义如下:
因此,文本的集合可被表示为矩阵形式,每行对应一条文本,每列对应每个文本中出现的词令牌(如单个词)。
我们称向量化是将文本文档集合转换为数字集合特征向量的普通方法。 这种特殊思想(令牌化,计数和归一化)被称为 Bag of Words 或 “Bag of n-grams” 模型。 文档由单词出现来描述,同时完全忽略文档中单词的相对位置信息。
由于大多数文本文档通常只使用文本词向量全集中的一个小子集,所以得到的矩阵将具有许多特征值为零(通常大于99%)。为了能够将这样的矩阵存储在存储器中,并且还可以加速代数的矩阵/向量运算,实现通常将使用诸如 scipy.sparse 包中的稀疏实现。
类 CountVectorizer(即BOW) 在单个类中实现了 tokenization (词语切分)和 occurrence counting (出现频数统计):
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
# 对多条文本进行处理
corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?',
]
X = vectorizer.fit_transform(corpus)
# print(X)
# 对单一文本进行处理
analyze = vectorizer.build_analyzer()
print(analyze("This is a text document to analyze.")) # ['this', 'is', 'text', 'document', 'to', 'analyze']
# analyzer 在拟合过程中找到的每个 term(项)(每个单词)都会被分配一个唯一的整数索引,对应于 resulting matrix(结果矩阵)中的一列。此列的一些说明可以被检索如下:
# 获取每文本被分词后,形成的词袋
print(vectorizer.get_feature_names_out()) # ['and' 'document' 'first' 'is' 'one' 'second' 'the' 'third' 'this']
# 获取数字表示的句子
print(X.toarray())
# [[0 1 1 1 0 0 1 0 1]
# [0 1 0 1 0 2 1 0 1]
# [1 0 0 0 1 0 1 1 0]
# [0 1 1 1 0 0 1 0 1]]
# 获取某一单词在词袋中的序号
print(vectorizer.vocabulary_.get('document')) # 1
# 因此,在未来对 transform 方法的调用中,在 training corpus (训练语料库)中没有看到的单词将被完全忽略:
print(vectorizer.transform(['Something completely new.']).toarray()) # [[0 0 0 0 0 0 0 0 0]]
# 请注意,在前面的 corpus(语料库)中,第一个和最后一个文档具有完全相同的词,因为被编码成相同的向量。
# 特别是我们丢失了最后一个文件是一个疑问的形式的信息。
# 为了防止词组顺序颠倒,除了提取一元模型 1-grams(个别词)之外,我们还可以提取 2-grams 的单词:
bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\b\w+\b', min_df=1)
analyze = bigram_vectorizer.build_analyzer()
print(analyze('Bi-grams are cool!')) # ['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool']
X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
print(X_2)
# [[0 0 1 1 1 1 1 0 0 0 0 0 1 1 0 0 0 0 1 1 0]
# [0 0 1 0 0 1 1 0 0 2 1 1 1 0 1 0 0 0 1 1 0]
# [1 1 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 1 0 0 0]
# [0 0 1 1 1 1 0 1 0 0 0 0 1 1 0 0 0 0 1 0 1]]
from sklearn.feature_extraction.text import TfidfTransformer
transformer = TfidfTransformer(smooth_idf=False)
# 第一个词在任何时间都是100%出现,因此不是很有重要。另外两个特征只占不到50%的比例,因此可能更具有代表性:
counts = [[3, 0, 1],
[2, 0, 0],
[3, 0, 0],
[4, 0, 0],
[3, 2, 0],
[3, 0, 2]]
tfidf = transformer.fit_transform(counts)
print(tfidf.toarray())
# [[0.81940995 0. 0.57320793]
# [1. 0. 0. ]
# [1. 0. 0. ]
# [1. 0. 0. ]
# [0.47330339 0.88089948 0. ]
# [0.58149261 0. 0.81355169]]
# 若smooth_idf=True,将 “1” 添加到分子和分母
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(counts)
print(tfidf.toarray())
# [[0.85151335 0. 0.52433293]
# [1. 0. 0. ]
# [1. 0. 0. ]
# [1. 0. 0. ]
# [0.55422893 0.83236428 0. ]
# [0.63035731 0. 0.77630514]]
# 通过fit方法调用存储在模型属性中的每个特征的权重:
print(transformer.idf_)
# 由于 tf-idf 经常用于文本特征,所以还有一个类 TfidfVectorizer ,它将 CountVectorizer 和 TfidfTransformer 的所有选项组合在一个单例模型中:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?',
]
vectorizer.fit_transform(corpus)
关于TfidfVectorizer的参数:
文本由字符组成,但文件由字节组成。字节转化成字符依照一定的编码(encoding)方式。 为了在Python中使用文本文档,这些字节必须被解码为 Unicode 的字符集。对于现代文本文件,正确的编码可能是 UTF-8,因此它也是默认解码方式 (encoding=“utf-8”)。若出现编码相关错误“UnicodeDecodeError”,可参考文档解决。
词汇表达方式相当简单,但在实践中却非常有用。
词袋法无法捕获短语和多字表达,会忽略单词顺序依赖,不包含潜在的拼写错误或词汇导出。
因此,可使用N-Gram抢救。不要使用简单的unigrams集合 (n=1),而是使用(n=2)。
还可以考虑一个字符 n-gram 的集合,这是一种对拼写错误和派生有弹性的表示。
例如,假设我们正在处理两个文档的语料库: [‘words’, ‘wprds’]. 第二个文件包含 ‘words’ 一词的拼写错误。 一个简单的单词表示将把这两个视为非常不同的文档,两个可能的特征都是不同的。 然而,一个字符 2-gram 的表示可以找到匹配的文档中的8个特征中的4个,这可能有助于优选的分类器更好地决定:
# N-Gram补救关联和错误拼写问题
ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2))
counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
print(ngram_vectorizer.get_feature_names_out()) # [' w' 'ds' 'or' 'pr' 'rd' 's ' 'wo' 'wp']
print(counts.toarray().astype(int))
# [[1 1 1 0 1 1 1 0]
# [1 1 0 1 1 1 0 1]]
在上面的例子中,使用 'char_wb 分析器’,它只能从字边界内的字符(每侧填充空格)创建 n-gram。 ‘char’ 分析器可以创建跨越单词的 n-gram:
# analyzer char_wb和char对比
ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
ngram_vectorizer.fit_transform(['jumpy fox'])
print(ngram_vectorizer.get_feature_names_out()) # [' fox ' ' jump' 'jumpy' 'umpy ']
ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
ngram_vectorizer.fit_transform(['jumpy fox'])
print(ngram_vectorizer.get_feature_names_out()) # ['jumpy' 'mpy f' 'py fo' 'umpy ' 'y fox']
对于使用白色空格进行单词分离的语言,对于语言边界感知变体 char_wb 尤其有趣,因为在这种情况下,它会产生比原始 char 变体显着更少的噪音特征。 对于这样的语言,它可以增加使用这些特征训练的分类器的预测精度和收敛速度,同时保持关于拼写错误和词导出的稳健性。
虽然可以通过提取 n-gram 而不是单独的单词来保存一些本地定位信息,但是包含 n-gram 的单词和袋子可以破坏文档的大部分内部结构,因此破坏了该内部结构的大部分含义。
为了处理自然语言理解的更广泛的任务,因此应考虑到句子和段落的地方结构。因此,许多这样的模型将被称为 “结构化输出” 问题,这些问题目前不在 scikit-learn 的范围之内。
上述向量化方案是简单的,但是它从字符串令牌到整数特征索引的内存映射 ( vocabulary_ 属性)过程,在处理大型数据集时会引起几个问题 :
通过组合由 sklearn.feature_extraction.FeatureHasher 类实现的 “散列技巧” (特征哈希(相当于一种降维技巧)) 和 CountVectorizer 的文本预处理和标记化功能,可以克服这些限制。
这种组合是在 HashingVectorizer 中实现的,该类是与 CountVectorizer 大部分 API 兼容的变压器类。 HashingVectorizer 是无状态的,这意味着您不需要 fit 它:
# hash压缩词袋维度
from sklearn.feature_extraction.text import HashingVectorizer
hv = HashingVectorizer(n_features=10)
hv.transform(corpus)
# <4x10 sparse matrix of type '<... 'numpy.float64'>'
# with 16 stored elements in Compressed Sparse ... format>
你可以看到从向量输出中抽取了16个非0特征标记:与之前由CountVectorizer在同一个样本语料库抽取的19个非0特征要少。差异来自哈希方法的冲突,因为较低的n_features参数的值。
在真实世界的环境下,n_features参数可以使用默认值2 ** 20(将近100万可能的特征)。如果内存或者下游模型的大小是一个问题,那么选择一个较小的值比如2 ** 18可能有一些帮助,而不需要为典型的文本分类任务引入太多额外的冲突。
注意维度并不影响CPU的算法训练时间,这部分是在操作CSR指标(LinearSVC(dual=True), Perceptron, SGDClassifier, PassiveAggressive),但是,它对CSC matrices (LinearSVC(dual=False), Lasso(), etc)算法有效。
让我们再次尝试使用默认设置:
# 默认设置
hv = HashingVectorizer()
hv.transform(corpus)
# <4x1048576 sparse matrix of type '<... 'numpy.float64'>'
# with 19 stored elements in Compressed Sparse ... format>
冲突没有再出现,但是,代价是输出空间的维度值非常大。当然,这里使用的19词以外的其他词之前仍会有冲突。
类 HashingVectorizer 还具有以下限制:
参考文档
参考文档