IMDb(互联网电影数据库)影评数据集

Admin
发布于 2026-05-29 / 0 阅读
0
0
import os
import urllib.request
import tarfile

def download_and_extract(url, destination):
    if not os.path.exists(destination):
        os.makedirs(destination, exist_ok=True)
    file_path = os.path.join(destination, "aclImdb_v1.tar.gz")

    if not os.path.exists(file_path):
        print("Downloading the dataset...")
        urllib.request.urlretrieve(url, file_path)
        print("Download complete.")

    if "aclImdb" not in os.listdir(destination):
        print("Extracting the dataset...")
        with tarfile.open(file_path, 'r:gz') as tar:
            tar.extractall(path=destination)
        print("Extraction complete.")

# URL for the dataset
dataset_url = "http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"
download_and_extract(dataset_url, "./data")

这段Python代码的主要功能是**从指定的URL下载IMDb(互联网电影数据库)影评数据集的压缩包,并将其解压到本地指定的目录中**。这是一个在自然语言处理(NLP)和机器学习领域非常常见的数据准备步骤。

下面我将从实现原理、用途和注意事项三个方面为您详细解释:

### 一、 实现原理(代码逐行解析)

1. **导入必要的库**:

- os:用于处理文件和目录路径,以及检查文件是否存在、创建目录等。

- urllib.request:Python标准库中用于处理URL请求的模块,这里用于下载文件。

- tarfile:用于读取和写入tar归档文件(包括gzip压缩.tar.gz文件),这里用于解压。

2. **定义核心函数 download_and_extract(url, destination)**:

- **创建目标目录**:

```python

if not os.path.exists(destination):

os.makedirs(destination, exist_ok=True)

```

检查目标文件夹(./data)是否存在,如果不存在则创建exist_ok=True 表示如果目录已存在不会抛出异常(尽管外层已经做了判断,但加上这个参数是更健壮的写法)。

- **定义压缩包路径及下载逻辑**:

```python

file_path = os.path.join(destination, "aclImdb_v1.tar.gz")

if not os.path.exists(file_path):

urllib.request.urlretrieve(url, file_path)

```

拼接出压缩包的完整存储路径。通过 os.path.exists 检查文件是否已存在,**只有当文件不存在时才执行下载**。这是一种“断点续传”的简化实现,防止重复下载浪费时间和带宽。

- **解压逻辑**:

```python

if "aclImdb" not in os.listdir(destination):

with tarfile.open(file_path, 'r:gz') as tar:

tar.extractall(path=destination)

```

检查目标目录中是否已经存在解压后的文件夹 aclImdb。如果不存在,则使用 tarfile.open 以读取gzip压缩格式'r:gz')打开文件,并使用 extractall 将其解压到目标目录。同样,这避免了重复解压。

3. **执行代码**:

- 定义了斯坦福大学提供的IMDb数据集的官方URL。

- 调用函数,将数据下载并解压到当前目录下的 ./data 文件夹中。

---

### 二、 用途

1. **获取NLP经典数据集**:这段代码专门用于获取 **Large Movie Review Dataset (IMDb影评数据集)**。该数据集包含50,000条极度两极分化的电影评论,常用于**二分类情感分析**(判断影评是正面还是负面)。

2. **机器学习数据预处理**:在构建深度学习模型(如使用PyTorch、TensorFlow训练文本分类模型)之前,通常需要一个自动化脚本来获取和准备数据,这段代码就是数据管道的起点。

3. **通用文件下载与解压模板**:虽然它针对的是IMDb数据集,但稍微修改URL、文件名和解压逻辑,这段代码就可以作为下载和解压任何 .tar.gz 格式数据集的通用模板。

---

### 三、 注意事项与改进建议

1. **安全隐患(重要)**:

- **问题**tar.extractall() 存在潜在的路径遍历安全漏洞(CVE-2007-4559)。如果压缩包内包含恶意构造的文件(例如文件名为 ../../etc/passwd),解压时可能会覆盖系统重要文件。

- **改进**:在Python 3.12+ 中tarfile 新增了 filter 参数,建议使用 tar.extractall(path=destination, filter='data') 来过滤危险路径。如果使用较低版本Python,建议手动遍历 tar.getmembers() 并检查每个成员的路径是否合法。

2. **缺乏下载进度反馈**:

- **问题**urllib.request.urlretrieve 默认在下载大文件时没有任何进度提示,程序看起来像卡住了(该数据集约80MB)。

- **改进**:可以给 urlretrieve 传入一个 reporthook 回调函数来打印下载进度:

```python

def show_progress(block_num, block_size, total_size):

downloaded = block_num * block_size

progress = downloaded / total_size * 100

print(f"\rDownload progress: {progress:.1f}%", end="")

urllib.request.urlretrieve(url, file_path, show_progress)

```

3. **网络异常处理**:

- **问题**:代码中没有 try-except 块。如果网络断开或URL失效,程序会直接崩溃,且可能留下不完整的损坏文件。

- **改进**:添加异常捕获机制,如果下载失败则删除不完整的文件,并给出友好提示。

4. **解压判断逻辑的健壮性**:

- **问题**if "aclImdb" not in os.listdir(destination) 这个判断依赖于解压后的文件夹名称必须是 aclImdb。如果压缩包内部结构改变,或者目标目录里恰好有个同名的空文件夹,逻辑可能会出错。

- **改进**:更严谨的做法是检查解压后应该存在的某个特定核心文件是否存在,而不是仅仅检查目录名。

5. **更现代的替代方案**:

- 在实际的深度学习项目中,通常会使用更高级的库,例如 datasets (Hugging Face) 或 torchvision,它们内部已经封装了安全的下载、校验和解压逻辑。例如,使用Hugging Face datasets只需两行代码:

```python

from datasets import load_dataset

dataset = load_dataset("imdb")

```

总结来说,这段代码逻辑清晰,实现了基本的下载与解压功能,适合用于小型项目或学习演示。但在生产环境或对安全性、健壮性要求较高的项目中,需要针对上述注意事项进行升级改造。

from collections import Counter
import os

# Simple tokenizer
def tokenize(text):
    return text.lower().split()

# Build vocabulary
def build_vocab(path):
    counter = Counter()
    for folder in ["pos", "neg"]:
        folder_path = os.path.join(path, folder)
        for filename in os.listdir(folder_path):
            with open(os.path.join(folder_path, filename), 'r', encoding='utf-8') as file:
                counter.update(tokenize(file.read()))
    return {word: i+1 for i, word in enumerate(counter)}  # Starting index from 1

vocab = build_vocab("./data/aclImdb/train/")

下面我将为你详细解释这段Python代码的含义、实现原理、用途以及使用时的注意事项。

### 一、 代码含义概述

这段代码的主要功能是**读取文本数据集(IMDB影评数据集),对文本进行分词,并统计所有单词的出现频率,最终构建一个将单词映射为整数索引的词汇表**。这是自然语言处理(NLP)任务中最基础且关键的“数据预处理”步骤。

### 二、 实现原理与逐行解析

1. 导入依赖库

```python

from collections import Counter

import os

```

- Counter:是Python标准库中专门用于计数的工具,可以像字典一样统计元素出现的次数。

- os:用于处理文件路径和目录操作,保证代码在不同操作系统(Windows/Linux/Mac)上的兼容性。

2. 简单分词函数

```python

def tokenize(text):

return text.lower().split()

```

- 原理:首先将文本全部转换为小写lower()),这样可以避免同一个单词因大小写不同(如 "The" 和 "the")被统计为两个不同的词。然后使用 split() 按空白符(空格、换行等)将句子切分成单词列表。

- 注意:这是一种非常粗糙的分词方式,没有处理标点符号(如 "hello!" 和 "hello" 会被视为两个词),也没有处理缩写(如 "don't")。

3. 构建词汇表函数

```python

def build_vocab(path):

counter = Counter()

for folder in ["pos", "neg"]: # 遍历正向和负向文件夹

folder_path = os.path.join(path, folder)

for filename in os.listdir(folder_path): # 遍历文件夹下的所有文件

with open(os.path.join(folder_path, filename), 'r', encoding='utf-8') as file:

counter.update(tokenize(file.read())) # 读取文件内容,分词后更新词频统计

return {word: i+1 for i, word in enumerate(counter)} # 生成单词到索引的映射字典

```

- 原理

- 初始化一个空的 Counter 对象。

- 假设数据集结构为 train/pos/(正面评论)和 train/neg/(负面评论),代码依次遍历这两个子目录。

- 读取每一个文本文件,调用 tokenize 分词,并用 counter.update() 将这些词加入统计update 方法会自动累加相同单词的频次。

- 最后,通过字典推导式生成词汇表enumerate(counter) 会按照**词频从高到低**的顺序Counter的默认迭代顺序)为每个单词分配一个序号i+1 的设计是为了将索引从 1 开始(0 通常留给后续模型中的填充符 <PAD> 或未知符 <UNK>)。

4. 执行构建

```python

vocab = build_vocab("./data/aclImdb/train/")

```

- 指定本地IMDB训练集的路径,执行上述逻辑,返回词汇表字典并赋值给 vocab

### 三、 代码用途

1. 文本向量化:机器学习模型无法直接处理字符串,必须将文本转换为数字vocab 字典就是映射的桥梁。例如,将句子 "this movie is great" 转换为 [12, 5, 8, 120] 这样的整数序列。

2. 词频过滤:由于 Counter 按词频排序,在实际使用 vocab 时,可以很方便地只取前 N 个高频词(如前 10000 个),过滤掉低频的拼写错误或生僻词,从而减小模型体积,防止过拟合。

3. Embedding 层输入:为 PyTorch 或 TensorFlow 中的 nn.Embedding 层提供输入张量。

### 四、 注意事项与改进建议

1. 分词过于简陋

- split() 不会剥离标点符号。建议使用正则表达式(如 re.findall(r'\b\w+\b', text))或成熟的分词库(如 NLTK, spaCy)来替代 tokenize 函数。

2. 缺少特殊标记

- 真正的NLP词汇表通常需要预留固定的索引给特殊字符。例如:索引 0 分配给 <PAD>(用于填充不同长度的句子),索引 1 分配给 <UNK>(代表词汇表外的未知词)。当前代码没有处理这些。

3. 低频词处理

- 当前代码将所有出现过的词都加入了词汇表,这会导致词汇表极其庞大且包含很多无意义的错别字。建议在生成字典前加一个频次阈值过滤,例如return {word: i+1 for i, (word, count) in enumerate(counter.items()) if count >= 5}

4. 内存与性能

- file.read() 会一次性读取整个文件。如果存在极大的单文件文本,可能导致内存激增。对于超大文件,建议逐行读取for line in file:)。

5. 编码问题

- 代码中指定了 encoding='utf-8',这对于大多数英文数据集是没问题的。但如果数据集中混入了非UTF-8编码的文件,会抛出 UnicodeDecodeError。可以增加异常处理或使用 errors='ignore' 参数。

改进后的词汇表构建参考:

from collections import Counter
import os
import re

def tokenize(text):
    # 使用正则表达式提取单词,忽略标点符号
    return re.findall(r'\b\w+\b', text.lower())

def build_vocab(path, min_freq=5):
    counter = Counter()
    for folder in ["pos", "neg"]:
        folder_path = os.path.join(path, folder)
        if not os.path.exists(folder_path): continue
        for filename in os.listdir(folder_path):
            file_path = os.path.join(folder_path, filename)
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as file:
                for line in file:
                    counter.update(tokenize(line))
    
    # 预留特殊标记:0给PAD,1给UNK
    vocab = {"<PAD>": 0, "<UNK>": 1}
    idx = 2
    for word, count in counter.items():
        if count >= min_freq:  # 过滤低频词
            vocab[word] = idx
            idx += 1
    return vocab

希望这个解释对你有所帮助!如果你还有其他关于代码或NLP的问题,随时可以问我。

from collections import Counter
import os
import re
import matplotlib.pyplot as plt
from wordcloud import WordCloud

# ==========================================
# 1. 数据处理核心函数 (使用你提供的代码)
# ==========================================
def tokenize(text):
    return re.findall(r'\b\w+\b', text.lower())

def build_vocab(path, min_freq=5):
    counter = Counter()
    for folder in ["pos", "neg"]:
        folder_path = os.path.join(path, folder)
        if not os.path.exists(folder_path): continue
        for filename in os.listdir(folder_path):
            file_path = os.path.join(folder_path, filename)
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as file:
                for line in file:
                    counter.update(tokenize(line))
    
    vocab = {"<PAD>": 0, "<UNK>": 1}
    idx = 2
    for word, count in counter.items():
        if count >= min_freq:
            vocab[word] = idx
            idx += 1
    return vocab, counter # 🔥 修改:同时返回词频统计器 Counter

# ==========================================
# 2. 新增:按情感分类统计词频
# ==========================================
def get_sentiment_word_counts(path):
    pos_counter = Counter()
    neg_counter = Counter()
    
    # 英文常见停用词,过滤掉无意义的词汇 (如 the, is, a)
    stop_words = set(['the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being', 
                      'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'shall', 
                      'should', 'may', 'might', 'must', 'can', 'could', 'i', 'me', 'my', 
                      'we', 'our', 'you', 'your', 'he', 'his', 'she', 'her', 'it', 'its', 
                      'this', 'that', 'these', 'those', 'am', 'in', 'to', 'of', 'and', 
                      'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 
                      'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 
                      'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 
                      'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 
                      'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 
                      'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'just', 'don', 
                      'now', 'd', 'll', 'm', 'o', 're', 've', 'y', 'ain', 'aren', 'couldn', 
                      'didn', 'doesn', 'hadn', 'hasn', 'haven', 'isn', 'ma', 'mightn', 
                      'mustn', 'needn', 'shan', 'shouldn', 'wasn', 'weren', 'won', 'wouldn',
                      'movie', 'film', 'one', 'character', 'story', 'scene']) # 移除与情感无关的电影通用词

    for folder, counter in [("pos", pos_counter), ("neg", neg_counter)]:
        folder_path = os.path.join(path, folder)
        if not os.path.exists(folder_path): continue
        for filename in os.listdir(folder_path):
            file_path = os.path.join(folder_path, filename)
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as file:
                tokens = tokenize(file.read())
                # 过滤停用词
                filtered_tokens = [t for t in tokens if t not in stop_words]
                counter.update(filtered_tokens)
                
    return pos_counter, neg_counter

# ==========================================
# 3. 执行逻辑与可视化
# ==========================================
dataset_path = "./data/aclImdb/train/"

if os.path.exists(dataset_path):
    print("正在构建词汇表并统计词频...")
    vocab, total_counter = build_vocab(dataset_path)
    print(f"词汇表大小: {len(vocab)}")
    
    print("正在按情感分类统计词频...")
    pos_counts, neg_counts = get_sentiment_word_counts(dataset_path)
    
    # --- 可视化 1: 词汇表词频分布 (长尾效应) ---
    top_30_words = total_counter.most_common(30)
    words, counts = zip(*top_30_words)
    
    plt.figure(figsize=(12, 5))
    plt.subplot(1, 2, 1)
    plt.bar(words, counts, color='steelblue')
    plt.xticks(rotation=45, ha='right')
    plt.title("Top 30 Most Frequent Words in IMDB")
    plt.ylabel("Frequency")
    
    # --- 可视化 2: 情感词云图对比 ---
    # 生成正面评论词云
    wc_pos = WordCloud(width=800, height=400, background_color='white', max_words=50).generate_from_frequencies(pos_counts)
    # 生成负面评论词云
    wc_neg = WordCloud(width=800, height=400, background_color='black', max_words=50, colormap='Reds').generate_from_frequencies(neg_counts)
    
    plt.subplot(1, 2, 2)
    plt.imshow(wc_pos, interpolation='bilinear')
    plt.axis("off")
    plt.title("Positive Reviews Word Cloud")

    plt.figure(figsize=(6, 4))
    plt.imshow(wc_neg, interpolation='bilinear')
    plt.axis("off")
    plt.title("Negative Reviews Word Cloud")
    
    plt.show()
else:
    print(f"❌ 找不到数据集路径: {dataset_path}")
    print("请确保已将 IMDB 数据集解压到 ./data/aclImdb/ 目录下。")
def text_to_sequence(text, vocab):
    return [vocab.get(token, 0) for token in tokenize(text)]  # 0 for unknown words

def pad_sequences(sequences, maxlen):
    return [seq + [0] * (maxlen - len(seq)) if len(seq) < maxlen else seq[:maxlen] for seq in sequences]

# Example use
text = "This is an example."
seq = text_to_sequence(text, vocab)
padded_seq = pad_sequences([seq], maxlen=256)  # Example maxlen
print(seq)

[316, 3, 108, 34089]

这段代码是自然语言处理(NLP)任务中非常经典的数据预处理流程,主要用于将原始文本转换为深度学习模型可以接受的数字矩阵。

下面我将从**代码含义**、**实现原理**、**用途**和**注意事项**四个方面为你详细解释。

### 一、 代码含义逐行解析

1. *text_to_sequence(text, vocab) 函数**

- **功能**:将一段文本转换为一个整数索引序列。

- tokenize(text):将文本切分成词或子词的列表(注:此函数在代码中未定义,通常由第三方库如 nltk, spaCy 或自定义函数实现)。

- vocab.get(token, 0):查找当前词在词表 vocab 中的索引。如果词不在词表中(即未知词/Out-of-Vocabulary, OOV),则默认返回 0

- 整体使用列表推导式,返回一个由整数组成的列表。

2. *pad_sequences(sequences, maxlen) 函数**

- **功能**:将多个长度不一的序列统一填充或截断到固定长度 maxlen

- seq + [0] * (maxlen - len(seq)):如果序列长度小于 maxlen,则在序列末尾补 0

- seq[:maxlen]:如果序列长度大于 maxlen,则截断保留前 maxlen 个元素。

- 同样使用列表推导式,返回一个形状统一的二维列表。

3. **示例部分**

- 对文本 "This is an example." 进行转换和填充(假设 vocab 已在外部定义)。

- padded_seq 的长度将被固定为 256。

---

### 二、 实现原理

这段代码的核心原理是**文本的数值化与张量化**,这是让计算机“看懂”文本的必经之路:

1. **离散映射(符号化 -> 数值化)**:计算机无法直接处理字符串,必须建立符号到整数的映射表(即 vocab 字典)。通过查表操作,将文本转化为离散的整数序列。

2. **规整化(变长 -> 定长)**:深度学习模型(如 RNN, CNN, Transformer)通常要求输入是按批次传递的张量,这就要求同一个 Batch 内的序列长度必须一致pad_sequences 通过**填充**和**截断**操作,将变长序列对齐到同一维度,从而能够堆叠成规整的矩阵。

---

### 三、 用途

这段代码广泛应用于各类 NLP 任务的数据预处理阶段,包括但不限于:

- **文本分类**:如情感分析、垃圾邮件检测(输入为定长索引序列,输出为类别)。

- **序列到序列任务**:如机器翻译、文本摘要(编码器和解码器的输入都需要经过此类处理)。

- **语言模型**:如 GPT、BERT 等大模型的微调与推理的底层数据准备。

---

### 四、 注意事项与改进建议

1. **缺失的 tokenize 函数**:

代码中调用了 tokenize(text),但未定义。实际使用时需要引入分词器,例如:

```python

def tokenize(text):

return text.lower().replace('.', ' .').split() # 简单的按空格和标点切分

```

2. **缺失的 vocab 字典**:

运行示例代码前,必须定义词表,例如vocab = {"this": 1, "is": 2, "an": 3, "example": 4, ".": 5}

3. **填充位置(Pre-padding vs Post-padding)**:

当前代码使用的是**后填充**,即在序列末尾补0。但在某些模型(特别是 RNN/LSTM)中,**前填充**(在序列开头补0)效果更好,因为模型最后读取的往往是有效的语义信息,而不是无意义的0。如果需要前填充,可修改为[0] * (maxlen - len(seq)) + seq

4. **截断位置**:

当前代码使用的是**截断前部**seq[:maxlen])。如果关键信息集中在文本末尾,这种截断会丢失重要特征。可以改为保留末尾信息seq[-maxlen:]

5. **填充值的掩码**:

补充的 0 没有实际语义,在输入到模型(如 Transformer)时,必须配合 **Attention Mask** 使用,告知模型忽略这些填充位置,否则会影响计算结果。

6. **工业级替代方案**:

在实际工程中,建议直接使用成熟的库(如 HuggingFace transformerstokenizer 或 Keras 的 keras.preprocessing.sequence.pad_sequences),它们不仅封装了这些基础操作,还支持更高级的功能(如动态填充、Attention Mask 自动生成等),效率和安全性更高。

---

### 五、 完整可执行代码示例

为了让你能直接运行并理解,我补充了缺失的部分:

```python

def tokenize(text):

# 简单的转小写、分离标点并按空格切分

return text.lower().replace('.', ' .').split()

def text_to_sequence(text, vocab):

return [vocab.get(token, 0) for token in tokenize(text)] # 0 for unknown words

def pad_sequences(sequences, maxlen):

return [seq + [0] * (maxlen - len(seq)) if len(seq) < maxlen else seq[:maxlen] for seq in sequences]

# 模拟词表

vocab = {"this": 1, "is": 2, "an": 3, "example": 4, ".": 5}

# Example use

text = "This is an example."

seq = text_to_sequence(text, vocab)

print("原始索引序列:", seq) # 输出: [1, 2, 3, 4, 5]

padded_seq = pad_sequences([seq], maxlen=10) # 缩小maxlen以便观察

print("填充后序列:", padded_seq) # 输出: [[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]]

padded_seq_short = pad_sequences([seq], maxlen=3) # 测试截断

print("截断后序列:", padded_seq_short) # 输出: [[1, 2, 3]]

# Less naive tokenization with words in frequency order
from collections import Counter
import os

# Simple tokenizer
def tokenize(text):
    return text.lower().split()

# Build vocabulary
def build_vocab(path):
    counter = Counter()
    for folder in ["pos", "neg"]:
        folder_path = os.path.join(path, folder)
        for filename in os.listdir(folder_path):
            with open(os.path.join(folder_path, filename), 'r', encoding='utf-8') as file:
                counter.update(tokenize(file.read()))

    # Sort words by frequency in descending order
    sorted_words = sorted(counter.items(), key=lambda x: x[1], reverse=True)

    # Create vocabulary with indices starting from 1
    vocab = {word: idx + 1 for idx, (word, _) in enumerate(sorted_words)}
    vocab['<pad>'] = 0  # Add padding token with index 0
    return vocab

vocab = build_vocab("./data/aclImdb/train/")

这段代码实现了一个基于词频的自然语言处理(NLP)基础任务:**构建词汇表**。它读取文本数据,统计词频,并按照词频从高到低的顺序为每个词分配一个唯一的整数索引。

以下是详细的解释:

### 1. 实现原理

代码的执行流程可以分为以下几个步骤:

1. 分词tokenize 函数将输入的文本字符串转换为小写,并按空格切分成单词列表。

2. 词频统计build_vocab 函数遍历指定路径下的 pos(正面)和 neg(负面)文件夹中的所有文件,读取文本并使用 collections.Counter 统计所有单词在整个语料库中出现的总次数。

3. 排序:将统计好的词频字典转换为元组列表,并按词频(元组的第二个元素)降序排列。

4. 构建索引字典:遍历排序后的列表,从索引 1 开始依次为每个单词分配整数 ID。词频越高的词,索引越小。最后,手动将特殊标记 <pad> 的索引设为 0

### 2. 用途

这段代码通常用于深度学习/NLP模型的**数据预处理阶段**(特别是情感分析任务,从路径 aclImdb 可以看出这是在处理经典的IMDB电影评论数据集)。

- 文本向量化:深度学习模型无法直接处理字符串,必须将文本转换为整数索引。这个 vocab 字典就是映射的桥梁(如 vocab['movie'] = 15)。

- 按频次排序的意义:在后续模型训练时,如果需要截断过长的句子,或者只保留最常见的 Top-K 个词,按频次排序可以确保高频且通常更重要的词被保留,而低频词(或罕见词)被过滤掉(通常映射为 <unk>)。

- Padding机制:在将变长句子组合成一个 Batch 输入给神经网络时,通常需要将它们补齐到相同长度。索引为 0<pad> 标记就是用来做填充的,后续在计算损失函数时,可以通过 ignore_index=0 来忽略填充部分的损失。

### 3. 注意事项与局限性

虽然这段代码被称为 "Less naive"(不那么朴素)的切词方法,但在实际工业级应用中,仍存在以下需要注意的问题:

1. 分词过于简单

- text.lower().split() 仅按空格切分,它无法处理标点符号(如 "hello!""hello" 会被视为不同的词),也无法处理缩写(如 "don't" 不会被正确拆分)。

- 改进建议:使用更成熟的分词工具,如 NLTK、spaCy,或者基于子词的分词器(如 HuggingFace 的 BERT Tokenizer,BPE,WordPiece 等)。

2. 缺乏对未知词的处理

- 当前的词汇表包含了训练集中的所有单词。但在测试集或实际应用中,一定会遇到词汇表中没有的词,这会导致代码报错或无法正确映射。

- 改进建议:在构建词汇表时,应加入 <unk>(未知词)标记,并在映射时设定默认返回 <unk> 的索引。

3. 内存与性能问题

- Counter.update() 在处理海量文本时可能会消耗大量内存。

- 代码一次性读取整个文件 file.read(),如果存在超大文件可能会导致内存溢出(OOM)。虽然对于IMDB这种小数据集没问题,但扩展性较差。

4. 大小写敏感度

- 代码使用了 lower() 统一转为小写,这有助于减少词汇量(让 Appleapple 合并在小数据集上是好习惯)。但在某些需要区分专有名词的场景中,这会丢失信息。

### 总结

这段代码提供了一个清晰、易懂的基线词汇表构建方法,适合用于学习和小型NLP项目(如IMDB情感分类)。但如果要应用于生产环境,建议使用更高级的分词算法并补充 <unk> 等必要的特殊标记。

# Less naive tokenization with words in frequency order
from collections import Counter
import os

# Simple tokenizer
def tokenize(text):
    return text.lower().split()

# Build vocabulary
def build_vocab(path):
    counter = Counter()
    for folder in ["pos", "neg"]:
        folder_path = os.path.join(path, folder)
        if not os.path.exists(folder_path): # 防止路径不存在报错
            continue
        for filename in os.listdir(folder_path):
            with open(os.path.join(folder_path, filename), 'r', encoding='utf-8', errors='ignore') as file:
                counter.update(tokenize(file.read()))

    # Sort words by frequency in descending order
    sorted_words = sorted(counter.items(), key=lambda x: x[1], reverse=True)

    # Create vocabulary with indices starting from 1
    vocab = {word: idx + 1 for idx, (word, _) in enumerate(sorted_words)}
    vocab['<pad>'] = 0  # Add padding token with index 0
    return vocab, sorted_words # 🔥 修改:同时返回排序好的词频列表,方便后续打印

# 🔥 修改:接收返回的排序列表
vocab, sorted_words = build_vocab("./data/aclImdb/train/")

# ==========================================
# 美观打印区域
# ==========================================

print(f"📊 词汇表总大小: {len(vocab)} 个词\n")

# -----------
# 方式一:查看高频词和低频词(最推荐,快速了解数据分布)
# -----------
print("🏆 【Top 20 高频词】(索引越小,频次越高):")
print("-" * 30)
for word, freq in sorted_words[:20]:
    idx = vocab[word]
    # 使用 f-string 格式化对齐:{idx:<5} 表示左对齐占5个字符宽度
    print(f"  索引: {idx:<5} 词频: {freq:<6} 词: '{word}'")

print("\n...\n")

print("📉 【Top 20 低频词】(出现次数最少的词):")
print("-" * 30)
for word, freq in sorted_words[-20:]:
    idx = vocab[word]
    print(f"  索引: {idx:<5} 词频: {freq:<6} 词: '{word}'")


# -----------
# 方式二:分段打印(适合查看特定范围的索引映射)
# -----------
print("\n📖 【索引 1~30 映射详情】(每行3个):")
print("-" * 50)
items_to_show = list(vocab.items())[1:31] # 跳过0的<pad>,看1-30
for i in range(0, len(items_to_show), 3):
    # 每次取出3个元素打印在一行
    chunk = items_to_show[i:i+3]
    # 生成类似 " 'the': 1  |  'and': 2  |  'a': 3 " 的格式
    line = "  |  ".join([f"'{word}': {idx}" for word, idx in chunk])
    print(f"  {line}")


# -----------
# 方式三:查找特定词汇的索引(最实用的调试方法)
# -----------
test_words = ['movie', 'film', 'bad', 'good', 'awesome', 'terrible', 'unknownword']
print("\n🔍 【特定词汇查询】:")
print("-" * 30)
for word in test_words:
    # 使用 get 方法,如果找不到返回 '<UNK>' 标识
    idx = vocab.get(word, "未收录(UNK)")
    print(f"  '{word}' -> 索引: {idx}")
📊 词汇表总大小: 251638 个词

🏆 【Top 20 高频词】(索引越小,频次越高):
------------------------------
  索引: 1     词频: 322198 词: 'the'
  索引: 2     词频: 159953 词: 'a'
  索引: 3     词频: 158572 词: 'and'
  索引: 4     词频: 144462 词: 'of'
  索引: 5     词频: 133967 词: 'to'
  索引: 6     词频: 104171 词: 'is'
  索引: 7     词频: 90527  词: 'in'
  索引: 8     词频: 70480  词: 'i'
  索引: 9     词频: 69714  词: 'this'
  索引: 10    词频: 66292  词: 'that'
  索引: 11    词频: 65505  词: 'it'
  索引: 12    词频: 50935  词: '/><br'
  索引: 13    词频: 47024  词: 'was'
  索引: 14    词频: 45102  词: 'as'
  索引: 15    词频: 42843  词: 'for'
  索引: 16    词频: 42729  词: 'with'
  索引: 17    词频: 39764  词: 'but'
  索引: 18    词频: 31619  词: 'on'
  索引: 19    词频: 30887  词: 'movie'
  索引: 20    词频: 29059  词: 'his'
...
  'good' -> 索引: 55
  'awesome' -> 索引: 1634
  'terrible' -> 索引: 494
  'unknownword' -> 索引: 未收录(UNK)
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...


评论