基于Transformer实现机器翻译

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

1.1 概述
神经网络模型是一类广泛应用于机器学习的算法,其主要特点在于通过模拟生物神经网络的结构和功能,实现对复杂数据的处理和模式识别。自从Hinton等人在20世纪80年代提出深度学习的概念以来,神经网络模型逐渐发展成为许多领域的核心技术。
1.2 主要特点
1.2.1 层次结构:
神经网络模型由多个神经元层级联组成,其中每一层的输出作为下一层的输入。层次结构使模型能够逐步提取数据的抽象特征,从而实现对复杂模式的识别。
这一特性特别适用于处理图像、语音等高维数据,使得模型在这些领域表现出色。
1.2.2 非线性激活函数:
通过在每个神经元后应用非线性激活函数,神经网络模型能够处理非线性数据。常用的激活函数包括ReLU、sigmoid和tanh等。
这种机制使得模型能够拟合复杂的非线性关系,从而提高预测精度。
1.2.3 反向传播算法:
反向传播算法是一种用于训练神经网络的高效方法,通过计算损失函数的梯度并更新模型参数,使得模型能够逐步逼近最优解。
这一算法显著加速了模型的训练过程,使得大规模神经网络的训练成为可能。
1.2.4 正则化技术:
为了防止模型过拟合,常用的正则化技术包括Dropout、L2正则化等。这些技术通过引入随机性或限制模型复杂度,提高了模型的泛化能力。
正则化技术在深度学习中尤为重要,特别是在处理大规模数据集时。
1.2.5 可扩展性和灵活性:
神经网络模型的可扩展性使其适用于多种应用场景,通过调整网络结构和超参数,可以不断优化模型性能。
这一特性使得神经网络成为广泛应用于图像识别、语音识别、自然语言处理等领域的基础技术。
1.3 应用领域
图像识别:在图像分类、目标检测等任务中,神经网络模型表现出色,广泛应用于自动驾驶、安防等领域。
语音识别:用于将语音信号转换为文本,应用于语音助手、翻译等场景。
自然语言处理:在文本分类、情感分析、机器翻译等任务中,神经网络模型展现出强大的能力。
强化学习:在游戏、机器人控制等领域,通过与环境的交互学习,神经网络模型实现了自我优化和决策。

基于 Transformer 实现机器翻译(日译中)

1 环境及软件包

首先,让我们确保我们的系统中安装了以下软件包,如果发现缺少某些软件包,请确保安装它们。


	# 导入数学库
	import math
	# 导入torchtext库
	import torchtext
	# 导入torch库
	import torch
	# 导入torch.nn库
	import torch.nn as nn
	# 从torch中导入Tensor类
	from torch import Tensor
	# 从torch.nn.utils.rnn中导入pad_sequence函数
	from torch.nn.utils.rnn import pad_sequence
	# 从torch.utils.data中导入DataLoader类
	from torch.utils.data import DataLoader
	# 导入collections库中的Counter类
	from collections import Counter
	# 从torchtext.vocab中导入Vocab类
	from torchtext.vocab import Vocab
	# 从torch.nn中导入TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer类
	from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer
	# 导入io库
	import io
	# 导入time库
	import time
	# 导入pandas库
	import pandas as pd
	# 导入numpy库
	import numpy as np
	# 导入pickle库
	import pickle
	# 导入tqdm库
	import tqdm
	# 导入sentencepiece库
	import sentencepiece as spm
	# 设置随机种子
	torch.manual_seed(0)
	# 判断是否有可用的GPU,如果有则使用GPU,否则使用CPU
	device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
	# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码

device # 在编程中,可以使用 device 变量来指定使用哪个设备进行计算。例如,在 PyTorch 中,可以使用 device 变量将张量移动到 GPU 或 CPU 上进行计算。

2 获取平行数据集

在本教程中,我们将使用从 JParaCrawl [http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl] 下载的日英平行数据集,该数据集被描述为“由 NTT 创建的最大的可公开获取的英日平行语料库。它是通过在网上大规模爬取并自动对齐平行句子而创建的。”

# 读取名为 'zh-ja.bicleaner05.txt' 的文件,使用制表符作为分隔符,指定引擎为 Python,不包含表头信息
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 将第3列的数据转换为列表,赋值给 trainen 变量
trainen = df[2].values.tolist()#[:10000]
# 将第4列的数据转换为列表,赋值给 trainja 变量
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)
 

导入所有日语及其英文对应文本后,我删除了数据集中的最后一条数据,因为它有一个缺失值。总共,在 trainen 和 trainja 中的句子数量为 5,973,071 条,然而,为了学习目的,通常建议对数据进行抽样,并确保一切按预期工作正常,然后再一次使用所有数据,以节省时间。

以下是数据集中包含的一句话的示例。

print(trainen[500])
print(trainja[500])

运行结果:

我们还可以使用不同的平行数据集来跟随本文,只需确保我们可以将数据处理成上面所示的两个字符串列表,其中包含日语和英语句子。

3 准备分词器

与英语或其他字母语言不同,日语句子中没有空格来分隔单词。我们可以使用由 JParaCrawl 提供的标记工具,该工具使用 SentencePiece 为日语和英语创建,您可以访问 JParaCrawl 网站下载它们,或点击这里

en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')  # en_tokenizer用于处理英文文本
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')  # ja_tokenizer用于处理日文文本
# 这两个分词器都使用了预训练的模型文件,分别为 enja_spm_models/spm.en.nopretok.model 和 enja_spm_models/spm.ja.nopretok.model
 

载入分词器之后,你可以通过执行以下代码来测试它们。

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')

4 构建 TorchText 词汇对象,并将句子转换为 Torch 张量

使用标记器和原始句子,然后构建从 TorchText 导入的词汇对象。这个过程可能需要几秒钟或几分钟,这取决于我们数据集的大小和计算能力。不同的标记器也会影响构建词汇所需的时间,我尝试了几种其他的日语标记器,但 SentencePiece 似乎对我来说运行得很好且足够快。

# 定义一个函数,用于构建词汇表
def build_vocab(sentences, tokenizer):
    # 创建一个计数器对象
    counter = Counter()
    # 遍历输入的句子列表
    for sentence in sentences:
        # 使用分词器对句子进行编码,并将结果更新到计数器中
        counter.update(tokenizer.encode(sentence, out_type=str))
    # 返回一个词汇表对象,其中包含了计数器中的词汇,以及特殊符号:'<unk>', '<pad>', '<bos>', '<eos>'
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 使用训练集和分词器构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用训练集和分词器构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

有了词汇对象之后,我们就能够利用词汇表和分词器对象来构建训练数据的张量。

# 定义一个名为 data_process 的函数,接收两个参数:ja 和 en
def data_process(ja, en):
    # 初始化一个空列表 data
    data = []
    # 使用 zip 函数将 ja 和 en 中的元素一一对应地组合在一起,然后遍历这些组合
    for (raw_ja, raw_en) in zip(ja, en):
        # 对原始的日语文本进行分词,并将分词结果转换为词汇表中对应的索引值,然后将这些索引值转换为 LongTensor 类型的张量
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 对原始的英语文本进行分词,并将分词结果转换为词汇表中对应的索引值,然后将这些索引值转换为 LongTensor 类型的张量
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 将处理好的日语和英语张量作为元组添加到 data 列表中
        data.append((ja_tensor_, en_tensor_))
    # 返回处理后的数据列表
    return data

# 调用 data_process 函数处理训练数据,并将结果赋值给 train_data 变量
train_data = data_process(trainja, trainen)

5 创建 DataLoader 对象,在训练过程中进行迭代

这里,我将 BATCH_SIZE 设置为 16,以防止“cuda 内存不足”的问题,但这取决于诸如您的机器内存容量、数据大小等各种因素,因此根据您的需要随意更改批处理大小(注:PyTorch 的教程使用 Multi30k 德英数据集将批处理大小设置为 128)。

# 设置批处理大小为 8
BATCH_SIZE = 8
# 设置填充索引为日语词汇表中的 <pad> 对应的索引
PAD_IDX = ja_vocab['<pad>']
# 设置开始符号索引为日语词汇表中的 <bos> 对应的索引
BOS_IDX = ja_vocab['<bos>']
# 设置结束符号索引为日语词汇表中的 <eos> 对应的索引
EOS_IDX = ja_vocab['<eos>']

# 定义一个生成批处理数据的函数,输入为一个数据批次
def generate_batch(data_batch):
    # 初始化两个空列表,分别用于存储日语和英语的数据
    ja_batch, en_batch = [], []
    # 遍历输入的数据批次中的每个元素(日语和英语的句子对)
    for (ja_item, en_item) in data_batch:
        # 将开始符号、日语句子、结束符号拼接起来,并添加到日语数据列表中
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        # 将开始符号、英语句子、结束符号拼接起来,并添加到英语数据列表中
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
    # 对日语数据列表进行填充,使其长度相同,并用填充值替换短于最长句子的部分
    ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
    # 对英语数据列表进行填充,使其长度相同,并用填充值替换短于最长句子的部分
    en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
    # 返回填充后的日语和英语数据列表
    return ja_batch, en_batch

# 使用 DataLoader 加载训练数据,设置批处理大小为 8,打乱数据顺序,并使用 generate_batch 函数作为 collate_fn 参数
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)
 

6 序列到序列变压器

下面几行代码和文本解释(用斜体)取自原始的PyTorch教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]。我除了将BATCH_SIZE和单词de_vocab更改为ja_vocab外,没有做任何修改。

Transformer是一种Seq2Seq模型,介绍了“Attention is all you need”文档,用于解决机器翻译任务。Transformer模型包括一个编码器和一个解码器块,每个块包含固定数量的层。

编码器通过将输入序列传播到一系列多头注意力和前馈网络层来处理输入序列。编码器的输出称为内存,将其与目标张量一起馈送到解码器中。编码器和解码器使用教师强制技术进行端到端训练。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)

class Seq2SeqTransformer(nn.Module):
    def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
                 emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
                 dim_feedforward:int = 512, dropout:float = 0.1):
        super(Seq2SeqTransformer, self).__init__()
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
        
        self.generator = nn.Linear(emb_size, tgt_vocab_size)
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
        self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)
        self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)

    def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
                tgt_mask: Tensor, src_padding_mask: Tensor,
                tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
        src_emb = self.positional_encoding(self.src_tok_emb(src))
        tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)
        return self.generator(outs)

    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)
 

文本标记通过使用令牌嵌入表示。为了引入单词顺序的概念,位置编码被添加到令牌嵌入中。

class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
        super(PositionalEncoding, self).__init__()
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)
        pos_embedding = torch.zeros((maxlen, emb_size))
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        pos_embedding[:, 1::2] = torch.cos(pos * den)
        pos_embedding = pos_embedding.unsqueeze(-2)
        
        self.dropout = nn.Dropout(dropout)
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        return self.dropout(token_embedding +
                            self.pos_embedding[:token_embedding.size(0),:])

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.emb_size = emb_size

    def forward(self, tokens: Tensor):
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
 

我们创建一个后续的单词掩码,防止目标单词关注其后续的单词。我们还创建掩码,用于掩盖源和目标的填充标记。


def generate_square_subsequent_mask(sz):
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt):
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
    src_padding_mask = (src == PAD_IDX).transpose(0, 1)
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

定义模型参数并实例化模型。这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果请使用你自己的带GPU的电脑运行这一套代码。

当你使用自己的GPU的时候,NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。


SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(en_vocab)
EMB_SIZE = 512
NHEAD = 8
FFN_HID_DIM = 512
BATCH_SIZE = 16
NUM_ENCODER_LAYERS = 3
NUM_DECODER_LAYERS = 3
NUM_EPOCHS = 16

transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

transformer = transformer.to(device)
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
optimizer = torch.optim.Adam(
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)

训练一个epoch


def train_epoch(model, train_iter, optimizer):
    model.train()
    losses = 0
    for idx, (src, tgt) in enumerate(train_iter):
        src = src.to(device)
        tgt = tgt.to(device)

        tgt_input = tgt[:-1, :]

        src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)

        optimizer.zero_grad()

        tgt_out = tgt[1:, :]
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        loss.backward()

        optimizer.step()
        losses += loss.item()
    return losses / len(train_iter)

评估模型

def evaluate(model, val_iter):
    model.eval()
    losses = 0
    for idx, (src, tgt) in enumerate(val_iter):
        src = src.to(device)
        tgt = tgt.to(device)

        tgt_input = tgt[:-1, :]

        src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)
        tgt_out = tgt[1:, :]
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        losses += loss.item()
    return losses / len(val_iter)

7 开始训练

终于,在准备好必要的数据和功能之后,我们准备开始训练我们的模型了。训练过程中的时间消耗取决于多种因素,包括计算能力、模型参数以及数据集的大小和复杂性。

我使用了来自JParaCrawl的完整句子列表来训练模型。每种语言约有590万个句子。在单个NVIDIA GeForce RTX 3070 GPU上,每个时代大约需要5个小时。

以下是训练过程中的代码示例:

 
import tqdm
import time

# 使用tqdm库显示训练进度条
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS + 1)):
    start_time = time.time()
    
    # 调用train_epoch函数进行一轮训练,返回训练损失值
    train_loss = train_epoch(transformer, train_iter, optimizer)
    
    end_time = time.time()
    
    # 打印当前轮次、训练损失值和耗时信息
    print(f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
          f"Epoch time = {(end_time - start_time):.3f}s")

尝试使用训练好的模型来翻译一个日语句子。首先,我们创建一个函数来处理新句子的翻译。这包括获取日语句子、分词、转换为张量、进行推理,然后将结果解码回一个英语句子。


# 定义贪婪解码函数
def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)
    src_mask = src_mask.to(device)
    
    # 编码源序列,获取内存表示
    memory = model.encode(src, src_mask)
    
    # 初始化目标序列,将起始符号作为第一个元素
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
    
    # 循环生成目标序列
    for i in range(max_len - 1):
        memory = memory.to(device)
        
        # 创建目标序列掩码
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
        tgt_mask = (generate_square_subsequent_mask(ys.size(0))
                    .type(torch.bool)).to(device)
        
        # 解码目标序列,获取输出
        out = model.decode(ys, memory, tgt_mask)
        out = out.transpose(0, 1)
        
        # 通过生成器计算概率分布
        prob = model.generator(out[:, -1])
        
        # 选择概率最大的单词作为下一个单词
        _, next_word = torch.max(prob, dim=1)
        next_word = next_word.item()
        
        # 将下一个单词添加到目标序列中
        ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)
        
        # 如果遇到结束符,则停止生成
        if next_word == EOS_IDX:
            break
    
    return ys

# 定义翻译函数
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()
    
    # 对源语句进行分词,并添加起始符和结束符
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]
    
    # 将分词后的序列转换为张量,并创建源序列掩码
    num_tokens = len(tokens)
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1))
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)
    
    # 使用贪婪解码生成目标序列
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
    
    # 将目标序列转换为字符串,并去除起始符和结束符
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")

8 调用翻译函数并传入所需的参数

现在我们可以直接调用翻译函数,并传入所需的参数来翻译日语句子为英语。

例1:

translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)

运行结果:

The equipment for soldering, brazing or welding HS code 8515 (including electric and gas types).

例2:

trainen.pop(5)

运行结果:

Error: trainen is not defined.

9 保存词汇对象和训练好的模型

最后,在训练完成后,我们将保存词汇对象和训练好的模型。

保存词汇对象:

# 导入pickle模块
import pickle

# 保存英语词汇表
with open('en_vocab.pkl', 'wb') as file:
    pickle.dump(en_vocab, file)

# 保存日语词汇表
with open('ja_vocab.pkl', 'wb') as file:
    pickle.dump(ja_vocab, file)

保存训练好的模型:

# 保存模型仅用于推断
torch.save(transformer.state_dict(), 'inference_model')

# 保存模型和检查点以便稍后恢复训练
torch.save({
    'epoch': NUM_EPOCHS,
    'model_state_dict': transformer.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': train_loss,
}, 'model_checkpoint.tar')

结论

通过本文的实践,我们展示了Transformer模型在日语到英语机器翻译任务中的应用。这一模型不仅提升了翻译的准确性和效率,还为研究和应用领域提供了新的思路和技术基础。我们鼓励读者深入研究Transformer模型的原理和应用,探索其在不同自然语言处理任务中的广泛应用潜力。
通过本文的学习和实践,希望读者能够深入理解Transformer模型的核心思想,并在实际项目中运用这一强大的技术解决复杂的自然语言处理挑战。

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值