今天的课程主要学习了两个方向:

  1. 无监督学习中的 DBSCAN 聚类算法;
  2. 文本处理中的 TF-IDF,以及使用 TF-IDF 实现《红楼梦》篇章搜索。

DBSCAN 处理的是“样本之间如何形成 समूह”,TF-IDF 处理的是“一个词在文本中的重要程度”。一个偏向数值数据,一个偏向文本数据,但它们都体现了机器学习中的核心思想:从数据中发现规律。


一、什么是聚类

在监督学习中,训练数据通常带有标签。例如:

身高、体重 → 男性或女性
房屋面积 → 房屋价格
邮件内容 → 垃圾邮件或正常邮件

而聚类属于无监督学习。它没有提前给出标签,算法需要根据数据之间的相似程度,自动把相近的数据分到同一组。

例如,给定一批没有标签的用户数据:

年龄、收入、消费金额

聚类算法可能自动发现:

第1组:年轻、收入较低、消费较少
第2组:中年、收入中等、消费稳定
第3组:收入较高、消费金额较大

DBSCAN 就是一种常见的聚类算法。


二、DBSCAN 算法

DBSCAN 的全称是 Density-Based Spatial Clustering of Applications with Noise,中文可以叫“基于密度的空间聚类算法”。

它的核心思想不是提前规定要分成几类,而是寻找数据密度比较高的区域。

如果某个区域中有许多样本彼此靠近,DBSCAN 就会把它们组成一个簇;如果某些样本离其他数据都很远,就可能被识别为噪声点。

与 K-Means 相比,DBSCAN 有几个明显特点:

不需要提前指定聚类数量;

可以发现不规则形状的簇;

能够识别离群点和噪声;

对参数比较敏感;

当数据密度差异很大时,效果可能不理想。


三、DBSCAN 中的重要概念

DBSCAN 主要使用两个参数:

eps
min_samples

1. eps

eps 表示邻域半径。

对于一个样本,算法会寻找距离它不超过 eps 的其他样本。

例如:

eps = 0.5

表示只把距离小于等于 0.5 的样本看作邻居。

eps 太小,很多样本找不到足够邻居,容易被判断为噪声;eps 太大,不同簇可能被连接到一起。

2. min_samples

min_samples 表示一个核心点的邻域内至少需要包含多少个样本。

min_samples = 5

表示一个样本的邻域中至少有 5 个样本时,才可能成为核心点。

参数较大时,聚类条件更严格;参数较小时,更容易形成簇,但也可能把噪声误判为正常数据。


四、核心点、边界点和噪声点

DBSCAN 会把数据点分成三种类型。

1. 核心点

如果一个样本的邻域内包含不少于 min_samples 个样本,那么它就是核心点。

核心点是形成聚类区域的中心。

2. 边界点

边界点附近的样本数量不足以成为核心点,但它位于某个核心点的邻域内,因此可以被归入该簇。

3. 噪声点

既不是核心点,也不属于任何核心点的邻域,就会被判定为噪声点。

图形上可以简单理解为:

密集区域内部:核心点
密集区域边缘:边界点
远离所有区域:噪声点

这也是 DBSCAN 能够识别异常数据的原因。


五、使用 Scikit-learn 实现 DBSCAN

安装并导入相关库:

import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons

这里使用 make_moons() 生成两个月牙形数据:

X, y = make_moons(
    n_samples=300,
    noise=0.05,
    random_state=42
)

使用 DBSCAN 聚类:

model = DBSCAN(
    eps=0.2,
    min_samples=5
)

labels = model.fit_predict(X)

绘制聚类结果:

plt.scatter(
    X[:, 0],
    X[:, 1],
    c=labels,
    cmap="viridis"
)

plt.xlabel("特征1")
plt.ylabel("特征2")
plt.title("DBSCAN聚类结果")
plt.show()

labels 保存每个样本所属的类别:

print(labels)

其中:

0、1、2...

表示不同的簇,而:

-1

表示噪声点。

可以统计聚类数量:

import numpy as np

cluster_labels = set(labels)

if -1 in cluster_labels:
    cluster_labels.remove(-1)

print("聚类数量:", len(cluster_labels))
print("噪声点数量:", np.sum(labels == -1))

六、DBSCAN 参数选择

DBSCAN 的效果很大程度上取决于 eps 和 min_samples。

例如:

for eps in [0.1, 0.2, 0.3, 0.5]:
    model = DBSCAN(
        eps=eps,
        min_samples=5
    )

    labels = model.fit_predict(X)

    print(
        f"eps={eps}, "
        f"噪声点数量={np.sum(labels == -1)}"
    )

当 eps 较小时,邻域范围较小,噪声点通常会增加;当 eps 较大时,聚类可能合并。

如果特征的量纲差异较大,应该先进行标准化:

from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X)

model = DBSCAN(
    eps=0.5,
    min_samples=5
)

labels = model.fit_predict(X_scaled)

例如年龄的范围可能是 18~60,而收入可能是几千到几万。如果不标准化,收入这个特征可能会因为数值范围大而主导距离计算。


七、什么是 TF-IDF

TF-IDF 是文本处理中常见的关键词权重方法,英文全称是 Term Frequency-Inverse Document Frequency。

它主要解决一个问题:

一个词在当前文本中出现很多次,但它到底重要不重要?

例如,在一本书中,“的”“是”“我们”等词出现次数很多,但它们无法很好地代表文章主题。相反,某些只在特定章节中出现的词,虽然次数不多,却可能非常重要。

TF-IDF 由两部分组成:

TF:词频
IDF:逆文档频率

最终计算:

TF-IDF = TF × IDF

八、TF 词频

TF 表示一个词在当前文档中的出现频率。

公式为:

TF(t, d) =
词语t在文档d中出现的次数
──────────────────────
文档d中所有词语的总数

例如,一篇文章一共有 100 个词,“宝玉”出现了 5 次:

TF(宝玉) = 5 / 100 = 0.05

词语在当前文档中出现越频繁,TF 通常越大。

但是,只使用词频有一个缺点:某些常见词在所有文档中都出现很多次,它们的区分能力并不强。


九、IDF 逆文档频率

IDF 用来衡量一个词在所有文档中的稀有程度。

公式常写成:

IDF(t) = log(
文档总数 / 包含词语t的文档数
)

如果一个词出现在几乎所有文档中,那么它的 IDF 较小;如果一个词只出现在少数文档中,IDF 就较大。

例如:

总文档数:100
包含“人物”一词的文档数:90
包含“判词”一词的文档数:5

那么:

“人物”的IDF较小
“判词”的IDF较大

这说明“判词”更能代表某些特定章节的内容。


十、TF-IDF 的意义

综合 TF 和 IDF:

TF-IDF = 词在当前文档中的频率
       × 词在整个文档集合中的稀有程度

一个词只有同时满足以下条件,TF-IDF 才会比较高:

  1. 在当前文档中出现次数较多;
  2. 在其他文档中并不常见。

因此,TF-IDF 很适合:

提取关键词;计算文章相似度;搜索相关文档;判断文本主题;新闻分类;垃圾邮件识别。


十一、使用 TfidfVectorizer

Scikit-learn 提供了 TfidfVectorizer:

from sklearn.feature_extraction.text import TfidfVectorizer

documents = [
    "我喜欢学习Python",
    "Python可以进行机器学习",
    "机器学习可以处理文本"
]

vectorizer = TfidfVectorizer()

matrix = vectorizer.fit_transform(
    documents
)

print(vectorizer.get_feature_names_out())
print(matrix.toarray())

fit_transform() 会完成两个步骤:

  1. 根据文档集合建立词汇表;
  2. 将每篇文本转换为 TF-IDF 数值向量。

由于 TfidfVectorizer 默认按空格划分英文单词,中文文本通常需要先进行分词。


十二、中文文本分词

安装 jieba:

pip install jieba

使用分词:

import jieba

text = "我喜欢学习机器学习"

words = jieba.lcut(text)

print(words)

结果类似:

["我", "喜欢", "学习", "机器学习"]

把分词结果用空格连接:

cut_text = " ".join(words)

print(cut_text)

然后再交给 TF-IDF:

documents = [
    " ".join(jieba.lcut("我喜欢学习Python")),
    " ".join(jieba.lcut("Python可以进行机器学习")),
    " ".join(jieba.lcut("机器学习可以处理文本"))
]

vectorizer = TfidfVectorizer()

matrix = vectorizer.fit_transform(
    documents
)

十三、读取《红楼梦》文本

课程中使用文件读取《红楼梦》内容。首先打开文本文件:

with open(
    "红楼梦.txt",
    "r",
    encoding="utf-8"
) as file:
    text = file.read()

print(text[:500])

使用 with open 的好处是,代码块结束后文件会自动关闭。

如果文本较大,可以按行读取:

lines = []

with open(
    "红楼梦.txt",
    "r",
    encoding="utf-8"
) as file:
    for line in file:
        line = line.strip()

        if line:
            lines.append(line)

也可以使用:

with open(
    "红楼梦.txt",
    "r",
    encoding="utf-8"
) as file:
    lines = file.readlines()

读取后,需要把整本书拆分成多个文档。最简单的方式是按照章节标题分割。


十四、按照章节拆分文本

如果每章以“第X回”开头,可以使用正则表达式识别章节:

import re

chapters = re.split(
    r"第[一二三四五六七八九十百千]+回",
    text
)

如果文本使用阿拉伯数字,也可以写:

chapters = re.split(
    r"第\d+回",
    text
)

实际文件中章节标题格式可能不同,因此需要先观察文本内容,再确定分割规则。

更简单的示例:

chapter_list = []
current_chapter = []

for line in lines:
    if line.startswith("第") and line.endswith("回"):
        if current_chapter:
            chapter_list.append(
                "\n".join(current_chapter)
            )

        current_chapter = [line]
    else:
        current_chapter.append(line)

if current_chapter:
    chapter_list.append(
        "\n".join(current_chapter)
    )

每个元素就代表一个章节,之后可以把每个章节当作一篇文档进行 TF-IDF 计算。


十五、使用 TF-IDF 搜索相关章节

假设已经得到章节列表:

chapters = [
    "第一回 ...",
    "第二回 ...",
    "第三回 ..."
]

先进行中文分词:

import jieba

cut_chapters = [
    " ".join(jieba.lcut(chapter))
    for chapter in chapters
]

创建 TF-IDF 矩阵:

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer()

chapter_matrix = vectorizer.fit_transform(
    cut_chapters
)

用户输入搜索关键词:

query = input(
    "请输入想搜索的内容:"
)

query_cut = " ".join(
    jieba.lcut(query)
)

把查询内容转换成向量:

query_vector = vectorizer.transform(
    [query_cut]
)

计算查询向量和每个章节之间的相似度:

from sklearn.metrics.pairwise import cosine_similarity

similarities = cosine_similarity(
    query_vector,
    chapter_matrix
).flatten()

找到最相关的章节:

top_indices = similarities.argsort()[::-1][:5]

for index in top_indices:
    print(
        f"章节索引:{index}, "
        f"相似度:{similarities[index]:.4f}"
    )
    print(chapters[index][:200])
    print("-" * 40)

这里使用余弦相似度:

cosine_similarity

它主要比较两个向量的方向是否接近。两个章节中出现的关键词越相似,向量方向通常越接近,相似度也越高。


十六、完整的文本搜索示例

import jieba
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

with open(
    "红楼梦.txt",
    "r",
    encoding="utf-8"
) as file:
    text = file.read()

chapters = text.split(
    "\n\n"
)

chapters = [
    chapter.strip()
    for chapter in chapters
    if chapter.strip()
]

cut_chapters = [
    " ".join(jieba.lcut(chapter))
    for chapter in chapters
]

vectorizer = TfidfVectorizer(
    max_df=0.95,
    min_df=1
)

chapter_matrix = vectorizer.fit_transform(
    cut_chapters
)

query = input(
    "请输入搜索内容:"
)

query_cut = " ".join(
    jieba.lcut(query)
)

query_vector = vectorizer.transform(
    [query_cut]
)

similarity = cosine_similarity(
    query_vector,
    chapter_matrix
).flatten()

best_index = similarity.argmax()

print("最相关的内容:")
print(chapters[best_index])
print(
    f"相似度:{similarity[best_index]:.4f}"
)

这段程序的处理流程是:

读取小说
  ↓
拆分章节
  ↓
中文分词
  ↓
计算章节TF-IDF
  ↓
将搜索内容转换为向量
  ↓
计算余弦相似度
  ↓
返回最相关章节

十七、TF-IDF 搜索的局限

TF-IDF 是基于词语统计的算法,并不能真正理解语义。

例如:

“贾宝玉喜欢林黛玉”
“林黛玉受到贾宝玉喜爱”

这两句话表达的意思比较接近,但用词顺序和词形不同,TF-IDF 的相似度未必很高。

此外,搜索结果还会受到以下因素影响:

分词是否准确;

停用词是否删除;

章节拆分是否正确;

查询词是否在文本中出现;

常见词是否被过滤;

文本长度差异是否过大。

如果查询词完全没有在章节中出现,TF-IDF 向量可能接近全 0,所有章节的相似度都可能很低。

因此,实际项目中可以加入:

if similarity[best_index] < 0.1:
    print("没有找到足够相关的内容")

TF-IDF 适合关键词搜索和基础文本相似度计算,但如果要理解上下文和语义,还需要使用词向量、BERT 等更复杂的方法。


十八、总结

今天的课程包括 DBSCAN 聚类和 TF-IDF 文本处理两部分。

DBSCAN 是一种基于密度的无监督聚类算法,不需要提前指定聚类数量,并且能够识别噪声点。它主要使用 eps 和 min_samples 两个参数,分别控制邻域范围和形成核心点所需要的最少样本数量。

TF-IDF 用来衡量词语在文本中的重要程度:

TF-IDF = TF × IDF

其中,TF 表示词语在当前文本中的出现频率,IDF 表示词语在整个文档集合中的稀有程度。一个词如果在当前文章中常出现,但在其他文章中并不常见,它的 TF-IDF 权重就会比较高。

将《红楼梦》拆分为多个章节后,可以把每章视为一个文档,通过中文分词、TF-IDF 向量化和余弦相似度计算,实现基础的篇章搜索。

今天的内容可以概括为:

DBSCAN:根据数据密度自动聚类
TF-IDF:计算词语重要程度
余弦相似度:比较文本向量相似程度

前者帮助我们发现数据中隐藏的分组,后者帮助我们从大量文本中找到最相关的内容。掌握这些基础算法后,就可以继续学习聚类评价、词向量、文本分类和更先进的语义搜索。

更多推荐