欢迎光临
我们一直在努力

机器学习实战:DBSCAN 聚类与 TF-IDF 文本搜索

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

  • 无监督学习中的 DBSCAN 聚类算法;
  • 文本处理中的 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 才会比较高:

  • 在当前文档中出现次数较多;
  • 在其他文档中并不常见。
  • 因此,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() 会完成两个步骤:

  • 根据文档集合建立词汇表;
  • 将每篇文本转换为 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:计算词语重要程度
    余弦相似度:比较文本向量相似程度

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

    赞(0)
    未经允许不得转载:171主机测评 » 机器学习实战:DBSCAN 聚类与 TF-IDF 文本搜索
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址