(自用)代码研读:TextCNN模型代码分析之utils.py

数据集:每一行是一条数据,每行以制表符分开数据和标签

代码代表了数据的预处理过程,词汇表的构建以及对应词向量矩阵构建

原始数据:先提取每一行制表符前的部分进行分词处理构建词汇表,后续将每一行数据数据变为(单词索引列表;标签;序列长度)这样的形式,例如:

contents 列表:
[
    ([1, 2, 3, 4], 0, 4),  # 示例数据:(单词索引列表, 标签, 序列长度)
    ([5, 6, 7, 8], 1, 4),
    ([9, 10, 11, 12], 0, 4)
]

然后进行批次的处理设计

最终进行词向量矩阵的构建

# coding: UTF-8
import os
import torch
import numpy as np
import pickle as pkl
from tqdm import tqdm
import time
from datetime import timedelta


MAX_VOCAB_SIZE = 10000  # 词表长度限制
UNK, PAD = '<UNK>', '<PAD>'  # 未知字,padding符号
"""
'<UNK>':表示未知单词(unknown token)。在构建词汇表时,可能会遇到一些在训练数据中没有出现过的单词。对于这些单词,我们使用 '<UNK>' 作为它们的替代标记。
'<PAD>':表示填充标记(padding token)。在处理变长序列(例如句子)时,为了将所有序列对齐到相同长度,我们通常在较短的序列末尾添加填充标记 '<PAD>'。
"""

#build_vocab 函数能够从给定的文本文件中构建一个词汇表,并返回一个包含单词和索引映射的字典。该词汇表用于将文本数据中的单词转换为模型可以处理的索引。
def build_vocab(file_path, tokenizer, max_size, min_freq):#file_path:包含文本数据的文件路径。tokenizer:一个分词函数,用于将文本拆分成单词或字符。max_size:词汇表的最大长度。min_freq:单词在文本中出现的最小频率,小于该频率的单词将被忽略。
    vocab_dic = {}#初始化:创建一个空的词汇表字典 vocab_dic
    with open(file_path, 'r', encoding='UTF-8') as f:#以只读模式打开指定路径的文件,并指定编码为 UTF-8。
        for line in tqdm(f):#使用 tqdm 包装文件对象,以便在读取文件时显示进度条。逐行处理:遍历文件中的每一行。
            lin = line.strip()#去除首尾空白:lin.strip() 去除行首尾的空白字符。
            if not lin:#跳过空行:如果行为空,继续处理下一行。
                continue
            content = lin.split('\t')[0]#提取内容:假设每行以制表符 \t 分隔,取第一个部分 content 作为文本内容。
            for word in tokenizer(content):#分词:使用 tokenizer 函数将文本内容拆分成单词或字符。如果文本以空格隔开可以采用单词级分词,但是如果没有空格就采用字符级分词。
                vocab_dic[word] = vocab_dic.get(word, 0) + 1#统计频率:将每个单词出现的次数记录在 vocab_dic 字典中。如果单词已存在于字典中,则增加其计数,否则将其初始化为 1。
        vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] >= min_freq], key=lambda x: x[1], reverse=True)[:max_size]#构建词汇表:使用列表推导式过滤出出现频率大于或等于 min_freq 的单词。按单词出现频率降序排序。截取前 max_size 个单词:限制词汇表的大小不超过 max_size。
        vocab_dic = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)}#创建索引映射:枚举:使用 enumerate 函数为排序后的单词列表中的每个单词分配一个索引。字典推导式:创建一个新的词汇表字典 vocab_dic,键是单词,值是分配的索引。
        vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})#添加特殊标记:添加 '<UNK>' 标记:将 '<UNK>' 标记添加到词汇表中,其索引为当前词汇表的长度。添加 '<PAD>' 标记:将 '<PAD>' 标记添加到词汇表中,其索引为当前词汇表长度加 1。
    return vocab_dic#返回词汇表
""""
构建词汇表的过程包括分词、统计频率、过滤和排序、映射到索引,并添加特殊标记。
这一过程在代码中通过 build_vocab 函数实现,用于创建一个包含文本中所有唯一字符(或单词)的集合,并将其映射到索引。
"""


"""
整个 build_dataset 函数通过以下步骤构建数据集:
    根据 ues_word 参数选择分词方式(单词级或字符级)。
    构建或加载词汇表。
    定义嵌套函数 load_dataset,用于加载和处理数据集。
    读取数据文件,分词,填充或截断序列,将单词转换为索引,并存储处理后的数据。
    加载训练集、验证集和测试集。
    返回词汇表和处理后的数据集。

在 load_dataset 函数中,每一行文本被分词,并将每个单词转换为其在词汇表中的索引。
这些索引被存储在 words_line 列表中,并最终形成如下的 contents 列表:
[
    ([1, 2, 3, 4], 0, 4),  # 示例数据:(单词索引列表, 标签, 序列长度)
    ([5, 6, 7, 8], 1, 4),
    ([9, 10, 11, 12], 0, 4)
]
一行content代表:一整行句子分词后在词汇表中的索引表示;句子标签;句子长度

"""
def build_dataset(config, ues_word):#定义函数 build_dataset:接受两个参数,config(配置对象)和 ues_word(布尔值,决定是否使用基于单词的分词方式)。
    if ues_word:#如果 ues_word 为 True,使用基于空格的分词方式,将文本拆分为单词。如果 ues_word 为 False,使用基于字符的分词方式,将文本拆分为单个字符。
        tokenizer = lambda x: x.split(' ')  # 以空格隔开,word-level
    else:
        tokenizer = lambda x: [y for y in x]  # char-level
    if os.path.exists(config.vocab_path):#如果词汇表文件存在,加载现有的词汇表。
        vocab = pkl.load(open(config.vocab_path, 'rb'))
    else:#如果词汇表文件不存在,调用 build_vocab 函数构建词汇表,并将其保存到文件中。
        vocab = build_vocab(config.train_path, tokenizer=tokenizer, max_size=MAX_VOCAB_SIZE, min_freq=1)
        pkl.dump(vocab, open(config.vocab_path, 'wb'))
    print(f"Vocab size: {len(vocab)}")#打印词汇表的大小:显示词汇表中包含的单词数量。


    def load_dataset(path, pad_size=32):#定义嵌套函数 load_dataset:接受两个参数,path(数据文件路径)和 pad_size(填充后的序列长度,默认值为 32)。
        contents = []#初始化列表 contents:用于存储处理后的数据。
        with open(path, 'r', encoding='UTF-8') as f:#打开文件:以只读模式打开指定路径的文件,并指定编码为 UTF-8。
            for line in tqdm(f):#逐行读取文件:使用 tqdm 显示进度条。
                lin = line.strip()#去除首尾空白:lin.strip() 去除行首尾的空白字符。
                if not lin:#跳过空行:如果行为空,继续处理下一行。
                    continue
                content, label = lin.split('\t')#提取内容和标签:假设每行以制表符 \t 分隔,取出内容和标签。
                words_line = []#初始化 words_line:用于存储分词后的单词索引。
                token = tokenizer(content)#分词:使用 tokenizer 函数将内容拆分为单词或字符。
                seq_len = len(token)#计算序列长度:seq_len 为分词后的长度。
                if pad_size:#检查 pad_size:如果 pad_size 不为 0,则进行填充或截断。
                    if len(token) < pad_size:#如果序列长度小于 pad_size,在序列末尾添加填充标记 '<PAD>',直到序列长度为 pad_size。
                        token.extend([PAD] * (pad_size - len(token)))
                    else:#如果序列长度大于或等于 pad_size,截断序列,使其长度为 pad_size。
                        token = token[:pad_size]
                        seq_len = pad_size
                # word to id
                for word in token:#将单词转换为索引:遍历分词后的每个单词,将其转换为对应的索引。如果单词不在词汇表中,使用 '<UNK>' 标记的索引代替。
                    words_line.append(vocab.get(word, vocab.get(UNK)))
                contents.append((words_line, int(label), seq_len))#存储数据:将处理后的单词索引列表、标签和原始序列长度存储在 contents 列表中。
        return contents  # [([...], 0), ([...], 1), ...]#返回处理后的数据集
    train = load_dataset(config.train_path, config.pad_size)#分别调用 load_dataset 函数加载训练集、验证集和测试集。
    dev = load_dataset(config.dev_path, config.pad_size)
    test = load_dataset(config.test_path, config.pad_size)
    return vocab, train, dev, test#返回构建好的词汇表、训练集、验证集和测试集。

"""
DatasetIterater 类的作用是实现一个数据迭代器,用于批量加载数据并将其转换为 PyTorch 张量,以便在训练或评估模型时按批次处理数据。
它实现了 Python 的迭代器协议,提供了一种方便的方式来逐批次地访问数据。

数据转换方法:将每一个batch中的数据转换为 PyTorch 张量。   _to_tensor
提取并转换:
x:从 datas 中提取第一个元素(单词索引列表),转换为长整型张量,并移动到指定设备。
y:从 datas 中提取第二个元素(标签),转换为长整型张量,并移动到指定设备。
seq_len:从 datas 中提取第三个元素(序列长度),转换为长整型张量,并移动到指定设备。
返回值:返回一个包含 x 和 seq_len 的元组,以及 y。
datas = [
    ([1, 2, 3, 4], 0, 4),
    ([5, 6, 7, 8], 1, 4),
    ([9, 10, 11, 12], 0, 4)
]
x:tensor([[ 1,  2,  3,  4],
        [ 5,  6,  7,  8],
        [ 9, 10, 11, 12]], dtype=torch.int64).to(self.device)
y:tensor([0, 1, 0], dtype=torch.int64).to(self.device)
seq_len:tensor([4, 4, 4], dtype=torch.int64).to(self.device)


获取下一个批次的数据:next
-处理剩余数据:
如果有剩余数据且所有完整批次已处理完,提取剩余数据,更新索引,并转换为张量(利用_to_tensor)。
-处理所有批次已完成:
如果所有批次都已处理完,重置索引并抛出 StopIteration 异常。
-正常提取批次数据:
提取当前索引范围内的数据,更新索引,并转换为张量(利用_to_tensor)。
"""
class DatasetIterater(object):#定义了一个名为 DatasetIterater 的类,它是一个数据迭代器,用于批量加载数据并将其转换为 PyTorch 张量。
    def __init__(self, batches, batch_size, device):#__init__ 方法:初始化方法,接受三个参数:batches:包含所有数据的列表。batch_size:每个批次的大小。device:指定 PyTorch 张量所在的设备(例如 CPU 或 GPU)。
        self.batch_size = batch_size#self.batch_size:批次大小。
        self.batches = batches#self.batches:存储的所有批数据。
        self.n_batches = len(batches) // batch_size #self.n_batches:计算并存储批次数量(整除部分)。
        self.residue = False  # 记录batch数量是否为整数
        if len(batches) % self.n_batches != 0:
            self.residue = True
        self.index = 0#self.index:用于跟踪当前批次的索引。
        self.device = device#self.device:指定张量的设备。

    def _to_tensor(self, datas):#数据转换方法_to_tensor:将数据转换为 PyTorch 张量。
        x = torch.LongTensor([_[0] for _ in datas]).to(self.device)#x:从 datas 中提取第一个元素(单词索引列表),转换为长整型张量,并移动到指定设备。
        y = torch.LongTensor([_[1] for _ in datas]).to(self.device)#y:从 datas 中提取第二个元素(标签),转换为长整型张量,并移动到指定设备。

        # pad前的长度(超过pad_size的设为pad_size)
        seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)#seq_len:从 datas 中提取第三个元素(序列长度),转换为长整型张量,并移动到指定设备
        return (x, seq_len), y#返回值:返回一个包含 x 和 seq_len 的元组,以及 y。

    def __next__(self):#__next__方法:获取下一个批次的数据。
        if self.residue and self.index == self.n_batches:#self.residue:这是一个布尔值,表示数据是否不能被批次大小整除。如果 True,则表示有剩余的数据。self.index == self.n_batches:这是一个条件,表示当前索引是否已经到达最后一个完整批次。合起来,这个条件判断的意思是:如果有剩余的数据并且已经处理完所有完整的批次(即索引到达了完整批次的数量),则进入此条件分支处理剩余的数据。
            batches = self.batches[self.index * self.batch_size: len(self.batches)]#self.index * self.batch_size:计算当前索引所在的起始位置。len(self.batches):数据的总长度。这一行代码的意思是:提取从当前索引开始到数据结尾的所有剩余数据。
            self.index += 1#self.index:更新索引,表示已经处理了一个批次。
            batches = self._to_tensor(batches)#self._to_tensor(batches):调用 _to_tensor 方法,将提取的剩余数据转换为 PyTorch 张量。
            return batches#返回:返回转换后的批次数据。

        elif self.index >= self.n_batches:#self.index >= self.n_batches:如果所有批次都已处理完,重置索引并抛出 StopIteration 异常。
            self.index = 0
            raise StopIteration
        else:#其他情况:正常提取下一个批次的数据,并转换为张量。提取数据的范围为当前索引到下一个批次的索引范围。更新索引并返回批次数据。
            batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
            self.index += 1
            batches = self._to_tensor(batches)
            return batches

    def __iter__(self):#定义 __iter__ 方法:返回迭代器对象自身,使 DatasetIterater 类符合迭代器协议。
        return self

    def __len__(self):#定义 __len__ 方法:返回批次数量。如果有剩余的数据(self.residue 为 True),批次数量为 self.n_batches + 1。否则,批次数量为 self.n_batches。
        if self.residue:
            return self.n_batches + 1
        else:
            return self.n_batches


def build_iterator(dataset, config):#build_iterator 函数的作用是构建一个数据迭代器,用于批量加载数据。dataset:这是一个包含所有数据的列表。config:这是一个配置对象,包含批次大小(batch_size)和设备信息(device)等配置参数。
    iter = DatasetIterater(dataset, config.batch_size, config.device)#这里调用 DatasetIterater 类的构造函数,传入 dataset、config.batch_size 和 config.device,创建一个迭代器对象 iter。DatasetIterater 类会根据传入的 dataset、batch_size 和 device 创建一个可以批量加载数据的迭代器。
    return iter#返回创建好的迭代器对象 iter,这样调用者就可以使用这个迭代器对象来批量加载数据了。


def get_time_dif(start_time):#用于计算并返回从 start_time 到当前时间的时间差,返回结果是一个 timedelta 对象,表示经过的时间。
    """获取已使用时间"""
    end_time = time.time()
    time_dif = end_time - start_time
    return timedelta(seconds=int(round(time_dif)))


if __name__ == "__main__":
    '''提取预训练词向量'''
    # 下面的目录、文件名按需更改。
    train_dir = "./THUCNews/data/train.txt"#训练数据文件路径
    vocab_dir = "./THUCNews/data/vocab.pkl"#词汇表文件路径
    pretrain_dir = "./THUCNews/data/sgns.sogou.char"#预训练词向量文件路径
    emb_dim = 300#词向量的维度(这里是 300)
    filename_trimmed_dir = "./THUCNews/data/embedding_SougouNews"#保存处理后词向量文件的路径
    if os.path.exists(vocab_dir):#检查词汇表文件是否存在:如果存在,从文件中加载词汇表。如果不存在,使用 build_vocab 函数从训练数据构建词汇表,并保存到文件中。使用字符级分词(每个字符作为一个词)。
        word_to_id = pkl.load(open(vocab_dir, 'rb'))
    else:
        # tokenizer = lambda x: x.split(' ')  # 以词为单位构建词表(数据集中词之间以空格隔开)
        tokenizer = lambda x: [y for y in x]  # 以字为单位构建词表
        word_to_id = build_vocab(train_dir, tokenizer=tokenizer, max_size=MAX_VOCAB_SIZE, min_freq=1)
        pkl.dump(word_to_id, open(vocab_dir, 'wb'))

    embeddings = np.random.rand(len(word_to_id), emb_dim)#初始化词向量矩阵:len(word_to_id):词汇表的大小,即词汇表中单词的数量。emb_dim:词向量的维度。
    f = open(pretrain_dir, "r", encoding='UTF-8')#打开预训练词向量文件:以只读模式("r")打开文件,并指定编码为 UTF-8。
    for i, line in enumerate(f.readlines()):#逐行读取文件:遍历每一行。
        # if i == 0:  # 若第一行是标题,则跳过    其实这里第一行是预训练词向量的大小,也应该跳过,不过其实项目作者已经有现成的embedding了,因此直接用那个就好
        #     continue
        lin = line.strip().split(" ")#去除首尾空白:使用 strip 方法去除行首尾的空白字符(包括换行符)。按空格分割:使用 split(" ") 方法将行内容按空格分割成单词列表,结果存储在 lin 变量中。
        if lin[0] in word_to_id:#检查单词是否在词汇表中:如果分割后的第一个单词(lin[0])在词汇表 word_to_id 中。
            idx = word_to_id[lin[0]]#获取单词索引:通过 word_to_id 字典获取单词在词汇表中的索引,存储在 idx 变量中。
            emb = [float(x) for x in lin[1:301]]#提取词向量:将 lin 列表中从第 1 个到第 300 个元素(即单词向量部分)转换为浮点数,并存储在 emb 列表中。
            embeddings[idx] = np.asarray(emb, dtype='float32')#更新词向量矩阵:将 emb 列表转换为 numpy 数组,并存储到 embeddings 矩阵的对应位置(索引为 idx)。
    f.close()
    np.savez_compressed(filename_trimmed_dir, embeddings=embeddings)#保存 embeddings 矩阵:使用 np.savez_compressed 函数将 embeddings 矩阵保存为压缩文件,以节省存储空间。文件路径为 filename_trimmed_dir。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sparkling*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值