今天的课程主要学习了两个方向:
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 才会比较高:
因此,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() 会完成两个步骤:
由于 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:计算词语重要程度
余弦相似度:比较文本向量相似程度
前者帮助我们发现数据中隐藏的分组,后者帮助我们从大量文本中找到最相关的内容。掌握这些基础算法后,就可以继续学习聚类评价、词向量、文本分类和更先进的语义搜索。

![[特殊字符]DeepSeek‑Harness(DSH)小白保姆教程-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260816085112-6a817a009aabf-220x150.png)
