seq2seq概念和数据集处理

目录

1.seq2seq概念

2.数据

3.构建词表

4.数据集制作

5.划分数据集和验证集

一、seq2seq概念

Seq2Seq(全称 Sequence to Sequence)是一种深度学习模型架构,专门用于处理 输入和输出都是变长序列 的任务。

核心定义: 一种基于“编码器-解码器(Encoder-Decoder)”框架的模型,它通过一个中间状态(上下文向量)实现从源序列到目标序列的非对称映射。

在现实世界中,很多问题的答案并不是一个简单的标签,而是一串有节奏、有长度的信息,例如将一句英文翻译成法文,或者根据一段新闻生成摘要。Seq2Seq 的出现,打破了传统神经网络对输入输出长度必须固定的限制。

编码器-解码器架构

机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出, 我们可以设计一个包含两个主要组件的架构: 第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。 这被称为编码器-解码器(encoder-decoder)架构

  • 编码器:将输入编程成中间表达形式(特征)
  • 解码器:将中间表达形式(特征)解码为输出

我们以英语到法语的机器翻译为例: 给定一个英文的输入序列:“They”“are”“watching”“.”。 首先,这种“编码器-解码器”架构将长度可变的输入序列编码成一个“状态”, 然后对该状态进行解码, 一个词元接着一个词元地生成翻译后的序列作为输出: “Ils”“regordent”“.”。

序列到序列学习(seq2seq)

我们将使用两个循环神经网络的编码器和解码器, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务

遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。 为了连续生成输出序列的词元, 独立的循环神经网络解码器是基于输入序列的编码信息 和输出序列已经看见的或者生成的词元来预测下一个词元

特定的“”表示序列结束词元。 一旦输出序列生成此词元,模型就会停止预测。 在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先,特定的“”表示序列开始词元,它是解码器的输入序列的第一个词元。 其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。 例如,在 (Sutskever et al., 2014)的设计中, 正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 在其他一些设计中 编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于语言模型的训练, 可以允许标签成为原始的输出序列, 从源序列词元“”“Ils”“regardent”“.” 到新序列词元 “Ils”“regardent”“.”“”来移动预测的位置。

编码器是一个RNN,读取输入句子,可以是双向

解码器使用另外一个RNN来输出

编码器

编码器将长度可变的输入序列转换成 形状固定的上下文变量c, 并且将输入序列的信息在该上下文变量中进行编码。 可以使用循环神经网络来设计编码器。

考虑由一个序列组成的样本(批量大小是1)。 假设输入序列是x1,…,xTx_1, \ldots, x_Tx1,,xT, 其中xtx_txt是输入文本序列中的第t个词元。 在时间步t,循环神经网络将词元xtx_txt的输入特征向量xt\mathbf{x}_txtht−1\mathbf{h} _{t-1}ht1(即上一时间步的隐状态) 转换为ht\mathbf{h}_tht(即当前步的隐状态)。 使用一个函数f来描述循环神经网络的循环层所做的变换:
ht=f(xt,ht−1).\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}).ht=f(xt,ht1).

总之,编码器通过选定的函数q, 将所有时间步的隐状态转换为上下文变量:
c=q(h1,…,hT).\mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T).c=q(h1,,hT).

比如,当选择q(h1,…,hT)=hTq(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_Tq(h1,,hT)=hT时, 上下文变量仅仅是输入序列在最后时间步的隐状态hT\mathbf{h}_ThT

到目前为止,我们使用的是一个单向循环神经网络来设计编码器, 其中隐状态只依赖于输入子序列, 这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置 (包括隐状态所在的时间步)组成。 我们也可以使用双向循环神经网络构造编码器, 其中隐状态依赖于两个输入子序列, 两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列 (包括隐状态所在的时间步), 因此隐状态对整个序列的信息都进行了编码。

现在,让我们实现循环神经网络编码器。 注意,我们使用了嵌入层(embedding layer) 来获得输入序列中每个词元的特征向量。 嵌入层的权重是一个矩阵, 其行数等于输入词表的大小(vocab_size), 其列数等于特征向量的维度(embed_size)。 对于任意输入词元的索引i, 嵌入层获取权重矩阵的第i行(从0开始)以返回其特征向量。 另外,本文选择了一个多层门控循环单元来实现编码器。

解码器

正如上文提到的,编码器输出的上下文变量c 对整个输入序列x1,…,xTx_1, \ldots, x_Tx1,,xT进行编码。 来自训练数据集的输出序列y1,y2,…,yT′y_1, y_2, \ldots, y_{T'}y1,y2,,yT, 对于每个时间步t’(与输入序列或编码器的时间步t不同), 解码器输出yt′y_{t'}yt的概率取决于先前的输出子序列y1,…,yt′−1y_1, \ldots, y_{t'-1}y1,,yt1和上下文变量c, 即P(yt′∣y1,…,yt′−1,c)P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})P(yty1,,yt1,c)

为了在序列上模型化这种条件概率, 我们可以使用另一个循环神经网络作为解码器。 在输出序列上的任意时间步t′t^\primet, 循环神经网络将来自上一时间步的输出yt′−1y_{t^\prime-1}yt1 和上下文变量c作为其输入, 然后在当前时间步将它们和上一隐状态st′−1\mathbf{s}_{t^\prime-1}st1转换为 隐状态st′\mathbf{s}_{t^\prime}st。 因此,可以使用函数g来表示解码器的隐藏层的变换:
st′=g(yt′−1,c,st′−1).\mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}).st=g(yt1,c,st1).
在获得解码器的隐状态之后, 我们可以使用输出层和softmax操作 来计算在时间步t′t^\primet时输出yt′y_{t^\prime}yt的条件概率分布P(yt′∣y1,…,yt′−1,c)P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})P(yty1,,yt1,c)

当实现解码器时, 我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。 这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。 为了进一步包含经过编码的输入序列的信息, 上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。 为了预测输出词元的概率分布, 在循环神经网络解码器的最后一层使用全连接层来变换隐状态。

损失函数

在每个时间步,解码器预测了输出词元的概率分布。 类似于语言模型,可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化。

训练

在下面的循环训练过程中,特定的序列开始词元(“”)和 原始的输出序列(不包括序列结束词元“”) 拼接在一起作为解码器的输入。 这被称为强制教学(teacher forcing), 因为原始的输出序列(词元的标签)被送入解码器。 或者,将来自上一个时间步的预测得到的词元作为解码器的当前输入。

预测

为了采用一个接着一个词元的方式预测输出序列, 每个解码器当前时间步的输入都将来自于前一时间步的预测词元。 与训练类似,序列开始词元(“”) 在初始时间步被输入到解码器中。 当输出序列的预测遇到序列结束词元(“”)时,预测就结束了。

二、数据

在我们写代码之前, 我们需要先下载好要用到的资料: https://pan.baidu.com/s/1tpMJw3RoAyNm9B0im2REUg, 提取码: xqk3。

我们先固定随机性, 使训练可复现:

import numpy as np
import torch


def seed_everything(seed=42):
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False


seed_everything()

接下来, 我们需要构建出英语句子和法语句子:

import re

english_sentences = []  # 英语句子 x {x_1,x_2}
french_sentences = []  # 法语句子 y {y_1,y_2}

with open('fra.txt', 'r', encoding='utf-8') as f:
    for line in f:
        # 跳过空行
        if line.strip() == '':
            continue

        # 每一行都有三部分:英语、法语、注释,通过\t分隔
        # ['English sentence', 'French sentence', 'Comment']
        parts = line.strip().split('\t')
        # 因为我们可以看到fra.txt文本里面分为三部分, 分别是英语句子, 法语句子, 注释
        # 所以我们需要判断获得到的内容是否包含三部分, 如果不包含, 则跳过
        if len(parts) < 2:
            continue
        
        # 如果包含, 则将 parts[0] 和 parts[1] 这两个部分赋值给 english 和 french
        # 统一将英文和法语都小写处理(当然也可以都大写处理, 这个看你自己)
        english = parts[0].lower()
        french = parts[1].lower()

        # 在单词和符号之前插入空格 方便后续的分词操作
        compile = re.compile(r"([\w'])([.,!?])")  # 定义规则 获取两组内容 单词和符号
        english = compile.sub(r'\1 \2', english)  # 在第一组内容和第二组内容之间加入空格
        french = compile.sub(r'\1 \2', french)

        # 简单的文本清洗 移除特殊符号
        compile = re.compile(r"[^a-zA-Z0-9.,!?' ]")
        english = compile.sub('', english)
        french = compile.sub('', french)

        english_sentences.append(english)
        french_sentences.append(french)

len(english_sentences)  # 查看样本个数

运行结果:

237838

我们可以看到, 我们共获取到了237838个样本, 样本包含英语句子和法语句子。

我们抽前五个英语句子和法语句子, 查看一下:

english_sentences[:5], french_sentences[:5]

运行结果:

(['go .', 'go .', 'go .', 'go .', 'hi .'],
 ['va !', 'marche .', 'en route !', 'bouge !', 'salut !'])

从运行结果, 我们可以清晰的看到, 每五个英文对应的五个法语句子。因为我们之后要做的实验是通过英文句子来翻译成法语。

三、构建词表

接下来我们进入构建词表部分的代码:

from collections import Counter  # 容器 统计


# 词表类
class Vocabulary:

    def __init__(self, language: str) -> None:
        # 语言
        self.language = language
        # 词到索引
        self.word2idx = {"<pad>": 0, "<bos>": 1, "<eos>": 2, "<unk>": 3}
        # 索引到词
        self.idx2word = {0: "<pad>", 1: "<bos>", 2: "<eos>", 3: "<unk>"}
        # 词频统计
        self.word_count = Counter()
        # 总词数/索引值
        self.idx = 4

    def add_sentence(self, sentence: str):
        # 对句子进行分词,添加到词汇表中并统计出现的次数
        tokens = sentence.split()
        for token in tokens:
            self.word_count[token] += 1

    def build_vocab(self, min_freq=2):
        # 构建词汇表,过滤出现次数低于阈值的单词
        for token, freq in self.word_count.items():
            if freq >= min_freq:
                self.word2idx[token] = self.idx
                self.idx2word[self.idx] = token  # {索引值: 单词}
                self.idx += 1

    def sentence_to_indices(self, sentence: str):
        # 句子转索引数值
        return [
            self.word2idx.get(token, self.word2idx["<unk>"])
            for token in sentence.split()
        ]

    def indices_to_sentence(self, indices: list):
        # 索引数值转句子
        return [
            self.idx2word[idx] for idx in indices if idx not in {
                self.word2idx["<pad>"], self.word2idx["<bos>"],
                self.word2idx["<eos>"]
            }
        ]

    def __len__(self):
        # 词汇表大小
        return len(self.word2idx)


english_vocab = Vocabulary("english")
french_vocab = Vocabulary("french")

# 创建出来只有特殊词元,接下来我们需要添加样本进去,分词制作词汇表
for sentence in english_sentences:
    english_vocab.add_sentence(sentence)
for sentence in french_sentences:
    french_vocab.add_sentence(sentence)

# 构建映射字典
english_vocab.build_vocab()
french_vocab.build_vocab()

# 打印词汇表大小
print('英文词汇表大小:', len(english_vocab))
print('法语词汇表大小:', len(french_vocab))

print(list(english_vocab.word2idx.items())[:10])
print(list(english_vocab.idx2word.items())[:10])

print(list(french_vocab.word2idx.items())[:10])
print(list(french_vocab.idx2word.items())[:10])

这段代码定义并使用了 Vocabulary (词汇表)类。它是自然语言处理(NLP)任务中至关重要的一步,其核心作用是 在“人类语言(单词)”和“计算机语言(数字)”之间搭建一座桥梁

以下是对上一段代码的解析:

1. 核心定义:Vocabulary 类

这个类像是一个 双向字典 ,它维护了两个核心映射:

  • word2idx :单词 [ o bj ec tO bj ec t ] → 索引(训练时用,把句子转成数字序列)。
  • idx2word :索引 [ o bj ec tO bj ec t ] → 单词(预测后用,把数字序列转回人类能读懂的文字)。 关键组件解析:
  • 四个特殊词元(Special Tokens) :
    • (0): 填充符 。为了让同一个 Batch 里的句子长度一致,短句后面补 0。
    • (1): 起始符 。Begin of Sentence,告诉模型“开始翻译了”。
    • (2): 结束符 。End of Sentence,告诉模型“这句话说完了”。
    • (3): 未知词 。Unknown,如果遇到词汇表里没出现过的生僻词,统一用这个代替。
  • add_sentence :
    • 作用 :统计词频。它把句子拆开,数数每个词出现了几次。
  • build_vocab :
    • 作用 :正式编号。
    • 策略 :通过 min_freq 参数过滤掉只出现过 1 次的“噪音词”,只给高频词编号,这样可以减小模型体积,提高泛化能力。
  • sentence_to_indices :
    • 转换逻辑 :输入 “go .” [ o bj ec tO bj ec t ] → 输出 [4, 5] 。
2. 代码运行逻辑(流程演示)

我们可以把这段代码的执行过程比作**“编写一本双语词典”**:

  1. 实例化 :创建了 english_vocab 和 french_vocab 两本空白词典。
  2. 收集素材 ( add_sentence ) :遍历几十万条英法翻译对,记录每个词出现的频率。
  3. 编撰词典 ( build_vocab ) :
    • 过滤掉低频词。
    • 按照 0, 1, 2, 3, 4… 的顺序给单词排号。
  4. 结果展示 :
    • 英文词表大小 12111 :说明数据集中有 12111 个独特的英语单词(出现次数 [ o bj ec tO bj ec t ] ≥ 2 )。
    • 法语词表大小 22428 :说明法语的词汇量更大(或者变体更多)。

“这段代码实现了 文本向量化 的第一步。

首先,我们通过 Vocabulary 类定义了数据预处理的标准。它不仅处理普通单词,还通过 Special Tokens(特殊占位符) 解决了序列长度不一( )、识别边界( / )以及生僻词( )的问题。

在执行过程中,我们先对语料库进行 词频统计 ,然后基于 频率过滤 构建了一个紧凑的索引映射。最终,我们将‘英文句子’和‘法语句子’分别映射到了两个独立的数字空间,使得后续的 Seq2Seq 模型能够通过 Embedding 层(嵌入层)读取这些数字并进行高维空间的数学运算。”

接下来, 我们需要找出整个数据集中最长的一句话包含多少个单词(Token) :

# 找出最长的一句话包含多少个单词(Token)
max_token_num = 0
for i in range(len(english_sentences)):
    source_indices = english_vocab.sentence_to_indices(
        english_sentences[i])  # [4,5]
    target_indices = french_vocab.sentence_to_indices(french_sentences[i])
    if len(source_indices) > max_token_num:
        max_token_num = len(source_indices)
    if len(target_indices) > max_token_num:
        max_token_num = len(target_indices)
print(max_token_num)

运行结果:

66

四、数据集制作

接下来, 我们需要制作数据集, 用于训练模型:

from torch.utils.data import Dataset, DataLoader


class TranslationDataset(Dataset):

    def __init__(self,
                 source_sentences,
                 target_sentences,
                 source_vocab: Vocabulary,
                 target_vocab: Vocabulary,
                 max_len=100):
        super().__init__()

        self.source_sentences = source_sentences
        self.target_sentences = target_sentences
        self.source_vocab = source_vocab
        self.target_vocab = target_vocab
        self.max_len = max_len

    def __len__(self):
        return len(self.source_sentences)

    def __getitem__(self, idx: int):
        # 返回每个样本 x-y x和y的有效时间步

        # 得到指定索引的句子 idx=0
        source = self.source_sentences[idx]  # 'go .'
        target = self.target_sentences[idx]  # 'va !'

        # 句子转索引数值 分词+转索引
        source_indices = self.source_vocab.sentence_to_indices(source)  # [4,5]
        target_indices = self.target_vocab.sentence_to_indices(target)

        # 添加开始和结束标记
        source_indices = [
            self.source_vocab.word2idx['<bos>']
        ] + source_indices + [self.source_vocab.word2idx['<eos>']]  # [1,4,5,2]

        target_indices = [
            self.target_vocab.word2idx['<bos>']
        ] + target_indices + [self.target_vocab.word2idx['<eos>']]

        # 记录有用的时间步
        source_len = len(source_indices)
        target_len = len(target_indices)

        # 截断与填充 [1,4,5,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
        if source_len > self.max_len:
            source_indices = source_indices[:self.max_len]
            source_len = self.max_len
        else:
            source_indices = self._pad_sequence(source_indices)

        if target_len > self.max_len:
            target_indices = target_indices[:self.max_len]
            target_len = self.max_len
        else:
            target_indices = self._pad_sequence(target_indices)

        return {
            'source': torch.tensor(source_indices),
            'target': torch.tensor(target_indices),
            'source_len': source_len,
            'target_len': target_len
        }

    def _pad_sequence(self, sequence):
        return sequence + [self.source_vocab.word2idx['<pad>']
                           ] * (self.max_len - len(sequence))


dataset = TranslationDataset(english_sentences,
                             french_sentences,
                             english_vocab,
                             french_vocab,
                             max_len=70)
len(dataset)

这段代码实现了 PyTorch 中的 TranslationDataset (翻译数据集类) 。

如果说之前的 Vocabulary 类是“字典”,那么这个类就是一个 “流水线工厂” :它负责把原始的、长短不一的句子,加工成神经网络可以直接吞下的、规格统一的 张量(Tensor)

以下是代码逻辑的详细拆解和解释:

1. 核心任务:数据规格化

神经网络要求在一个批次(Batch)内,所有数据的形状必须一致。但自然语言有的长有的短,这个类的核心任务就是通过**“截断” 和 “填充”**来解决这个问题。

2. getitem 方法的“五步加工法”

当你调用 dataset[i] 时,工厂内部会经历以下五个步骤:

  • 第一步:取样与数值化
    • 拿到第 i 条英法句子对,调用 sentence_to_indices 把单词换成数字。
    • 例子 : “go .” [ o bj ec tO bj ec t ] → [4, 5] 。
  • 第二步:添加边界(关键!)
    • 在开头加上 (1),结尾加上 (2)。
    • 理由 :模型需要知道从哪里开始翻译,以及翻译到哪里该闭嘴。
    • 例子 : [1, 4, 5, 2] 。
  • 第三步:记录有效长度
    • 记录填充前的真实长度(如上面的 4)。
    • 理由 :模型在计算时需要知道哪些是真实信息,哪些是后面补的无意义的 0(即 Mask 机制)。
  • 第四步:对齐长度(截断与填充)
    • 截断 :如果句子超过了 max_len (70),就把多余的砍掉。
    • 填充 ( _pad_sequence ) :如果句子没到 70,就在后面补 (0)。
    • 例子 : [1, 4, 5, 2, 0, 0, 0, …] (总长度刚好 70)。
  • 第五步:打包输出
    • 把加工好的数字变成 PyTorch 的 tensor 格式返回。
3. 如何通俗地解释这段代码?

你可以把这段代码比作一个**“物流装箱员”**:

  1. 原材料 :长短不一的木棍(原始句子)。
  2. 打标签 :给每根木棍的头尾涂上红色( )和绿色( )油漆。
  3. 标准箱子 :所有的包装箱都必须是 70 厘米长( max_len=70 )。
  4. 装箱策略 :
    • 如果木棍太长(超过 70 厘米),就锯掉多出的部分。
    • 如果木棍太短,就往箱子里塞泡沫塑料( ),直到把 70 厘米塞满。
  5. 记录单 :在箱子上贴个便条,写着里面木棍的真实长度( source_len ),防止收货人把泡沫塑料也当成木棍。

运行结果:

237838

图片最下方显示 len(dataset) 为 237838 ,这代表你的工厂现在已经准备好了 23.7 万多个“标准包装箱”,随时可以喂给神经网络进行训练。

五、划分训练集和验证集

接下来我们需要划分数据集为训练集和验证集。训练集占比80%, 验证集占比20%。

train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(
    dataset, [train_size, val_size])
len(train_dataset), len(val_dataset)

接下来我们需要对数据集进行批量化数据处理:

from pandas.core.ops.docstrings import key


def collate_batch(batch):
    # 自定义数据处理函数 batch 是一个列表,每个元素是一个样本
    # [{ 'source': tensor[max_len], 'target': tensor,'source_len': 4,'target_len': 4},{},{}]
    # { 'source': [batch_size,max_len], 'target': tensor,'source_len': tensor,'target_len': tensor}
    batch.sort(key=lambda x: x['source_len'], reverse=True)

    # shape (batch_size, max_len)
    source = torch.stack([x['source'] for x in batch])
    target = torch.stack([x['target'] for x in batch])

    # shape (batch_size)
    source_len = torch.tensor([x['source_len'] for x in batch])
    target_len = torch.tensor([x['target_len'] for x in batch])

    return {
        'source': source,
        'target': target,
        'source_len': source_len,
        'target_len': target_len,
    }


BATCH_SIZE = 256
train_loader = DataLoader(train_dataset,
                          batch_size=BATCH_SIZE,
                          collate_fn=collate_batch)
val_loader = DataLoader(val_dataset,
                        batch_size=BATCH_SIZE,
                        collate_fn=collate_batch)
len(train_loader)

这段代码实现了 PyTorch 中非常关键的 批量化数据处理逻辑 。

简单来说,它的作用是: 把散乱的、单个的包裹(样本),按照特定的规则整齐地打包成一个个大货箱(Batch),以便送往显卡进行高效训练。

运行结果:

744

这说明了按照每个批次的大小(256),需要跑 744 趟训练。每个训练中,模型会接收 256 个样本,每个样本的原文和译文会被打包成一个大货箱,送往显卡进行高效训练。

以下是代码的解析:

1. 核心函数:collate_batch (整理员)

在 PyTorch 中, DataLoader 默认会尝试把数据简单的堆叠在一起。但对于 Seq2Seq(机器翻译)任务,我们需要更精细的操作,这就是 collate_batch 的作用:

  • batch.sort(…) —— 按长度排序 (最关键的一步)
    • 代码 : batch.sort(key=lambda x: x[‘source_len’], reverse=True)
    • 作用 :将这一个 Batch 里的 256 个句子,按照“英文原文的真实长度”从长到短排序。
    • 为什么? :这是为了配合 RNN(循环神经网络)的性能优化。PyTorch 的一些加速工具(如 pack_padded_sequence )要求输入序列必须按长度降序排列。这样模型处理时可以跳过后面的无效 0(Padding),极大地提高计算效率。
  • torch.stack(…) —— 矩阵堆叠
    • 代码 :将 256 个形状为 (70,) 的一维向量,叠成一个形状为 (256, 70) 的二维矩阵。
    • 结果 : source 矩阵就是模型一次性要读入的原文, target 矩阵是对应的译文标签。
  • 长度收集 :
    • 同时把这 256 个句子的真实长度也存成 Tensor。模型在计算 Loss(损失)时,需要知道哪些是补的 0,从而不计算 0 的误差。

2. DataLoader (搬运卡车)

  • BATCH_SIZE = 256 :每次给模型“喂” 256 组翻译对。
  • collate_fn=collate_batch :告诉卡车:“在装货时,请使用我上面写的那个‘整理员’来排队和打包,别瞎装。”
  • len(train_loader) = 744 :
    • 含义 :你的总样本量(约 19 万)除以 256,等于 744。
    • 直观理解 :完成一轮完整的训练(Epoch),模型需要跑 744 趟。

3. 如何通俗地解释这段代码?

你可以把这个过程比作**“快递分拣中心”**:

  1. 原材料 :流水线上过来的 256 个快递盒(单个样本),里面有长短不一的木头和一张写着长度的纸条。
  2. 分拣员 ( collate_batch ) :
    • 排序 :他把长木头排在左边,短木头排在右边。
    • 捆扎 :他用胶带把这 256 根木头捆成一个大方块( torch.stack )。
    • 贴单 :他在大方块外面贴上 256 张长度标签,告诉收货人每根木头的真实尺寸。
  3. 货运卡车 ( DataLoader ) :装满 256 捆货,准时发往“模型工厂”进行加工。

最后我们需要进行验收环节, 我们提取一个示例来检查验证:

# 提取一个示例来检查验证
examples = enumerate(train_loader)  # [(0,[特征,标签]),()]
batch_idx, batch_data = next(examples)
print(batch_idx)
print(batch_data['source'].shape)
print(batch_data['target'].shape)
print(batch_data['source_len'].shape)
print(batch_data['target_len'].shape)

运行结果:

0
torch.Size([256, 70])
torch.Size([256, 70])
torch.Size([256])
torch.Size([256])

我们再查看第一个样本的具体内容:

print(batch_data['source'][0])
print(batch_data['target'][0])
print(batch_data['source_len'][0])
print(batch_data['target_len'][0])

运行结果:

tensor([   1, 2303, 2015,   62,  158, 1839,  663, 2361,  306,  251,  753, 1723,
        1029,  130, 2015, 4970,   71, 2795,  306, 1205,    5,    2,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0])
tensor([    1,   418,   369, 19357,   245,   246,   255,  2808,  1000,   779,
          786,   186,  6539,    58,   369,   802,  6805,  3195,   130,  7435,
            7,     2,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0])
tensor(22)
tensor(22)

刚才那两段代码作用: 从已经构建好的数据流水线(DataLoader)中抽取一个批次(Batch)的数据,进行“开箱检查”,确保送到模型嘴边的“饭”是符合规格的。

以下是代码逻辑及输出结果的解析:

1. 核心操作:开箱验货

  • next(examples) :
    • 作用 :从 train_loader 中手动取出一个 Batch(包含 256 个样本)。
    • 比喻 :就像从工厂流水线上随机抽检一箱打包好的成品。
  • 打印 shape (形状检查) :
    • source.shape [ o bj ec tO bj ec t ] → [256, 70] :说明这箱货里有 256 个句子,每个句子都对齐到了 70 个单位长。
    • source_len.shape [ o bj ec tO bj ec t ] → [256] :说明这 256 个句子的原始长度都被记录下来了。

2. 细节透视:看一个具体的“包装盒”

代码进一步打印了 batch_data[‘source’][0] ,即第一个样本的具体内容,我们可以从中读出很多信息:

  • 起始符与结束符 :
    • 序列开头的 1 是 ,结尾的 2 是 。这验证了我们的 TranslationDataset 类工作正常。
  • 数值化的单词 :
    • 中间那一串数字(如 2303, 2015… )就是翻译内容的 ID。
  • 填充符(Padding) :
    • 后面出现了一大串 0 。这说明这个句子没达到 70 的上限,被自动补齐了。
  • 长度验证 :
    • 最后输出 tensor(22) 。这说明虽然这个序列在矩阵里占了 70 个坑位,但其中 真正包含语义的只有 22 个数字 (包括起始和结束符)。

“这段代码是对**数据流水线(Data Pipeline)**的最终校验 。

通过 next(iter(…)) 操作,我们确认了:

  1. 批处理能力 : DataLoader 能够成功将离散样本打包成 [256, 70] 的张量矩阵。
  2. 协议符合性 :每个样本都正确嵌入了 (1) 和 (2) 标记,且长短句通过 (0) 实现了对齐。
  3. 信息完整性 :我们同步维护了 source_len 向量,它记录了每个样本的有效长度(如图中的 22)。这对于后续模型忽略填充位(Masking)以及计算高效 Loss 至关重要。

    结果显示,数据已经完全准备就绪,可以输入到 Seq2Seq 模型中进行矩阵运算了。”

到这里为止, 我们已经完成了数据的制作以及划分好了数据集(训练集、验证集)。这是为了后续模型的训练和评估做铺垫, 打下夯实的基础。构建seq2seq模型, 训练和测试模型的内容我们将它放到下一篇文章里面。

好了, 我们的这篇关于seq2seq概念和数据集处理的这篇文章的内容就到此结束了。

以上就是seq2seq概念和数据集处理的所有内容了, 如果有哪里不懂的地方,可以把问题打在评论区, 欢迎大家在评论区交流!!!如果我有写错的地方, 望大家指正, 也可以联系我, 让我们一起努力, 继续不断的进步。学习是个漫长的过程, 需要我们不断的去学习并掌握消化知识点, 有不懂或概念模糊不理解的情况下,一定要赶紧的解决问题, 否则问题只会越来越多, 漏洞也就越老越大。人生路漫漫, 白鹭常相伴!!!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐