基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

栏目: 数据库 · 发布时间: 5年前

内容简介:机器翻译(machine translation, MT)是用计算机来实现不同语言之间翻译的技术。需要翻译的语言通常称为源语言(source language),翻译成的结果语言称为目标语言(target language)。机器翻译即实现从源语言到目标语言转换的过程,是自然语言处理的重要研究领域之一。本文将带领大家了解经典的端到端神经网络机器翻译 Seq2Seq 模型,以及如何用 PaddlePaddle 来训练。如果您想要实践效果更佳的翻译模型,请参考 GitHhub 模型库中 Transformer

机器翻译(machine translation, MT)是用计算机来实现不同语言之间翻译的技术。需要翻译的语言通常称为源语言(source language),翻译成的结果语言称为目标语言(target language)。机器翻译即实现从源语言到目标语言转换的过程,是自然语言处理的重要研究领域之一。

本文将带领大家了解经典的端到端神经网络机器翻译 Seq2Seq 模型,以及如何用 PaddlePaddle 来训练。如果您想要实践效果更佳的翻译模型,请参考 GitHhub 模型库中 Transformer 实现。

背景介绍

早期机器翻译系统多为基于规则的翻译系统,需要由语言学家编写两种语言之间的转换规则,再将这些规则录入计算机。该方法对语言学家的要求非常高,而且我们几乎无法总结一门语言会用到的所有规则,更何况两种甚至更多的语言。因此统计机器翻译(Statistical Machine Translation, SMT)技术应运而生。

在统计机器翻译技术中,转化规则是由机器自动从大规模的语料中学习得到的,而非我们人主动提供规则。因此,它克服了基于规则的翻译系统所面临的知识获取瓶颈的问题,但仍然存在许多挑战:1)人为设计许多特征(feature),但永远无法覆盖所有的语言现象;2)难以利用全局的特征;3)依赖于许多预处理环节,如词语对齐、分词或符号化(tokenization)、规则抽取、句法分析等,而每个环节的错误会逐步累积,对翻译的影响也越来越大。

近年来,深度学习技术的发展为解决上述挑战提供了新的思路。将深度学习应用于机器翻译任务的方法大致分为两类:1)仍以统计机器翻译系统为框架,只是利用神经网络来改进其中的关键模块,如语言模型、调序模型等(见图 1 的左半部分);2)不再以统计机器翻译系统为框架,而是直接用神经网络将源语言映射到目标语言,即端到端的神经网络机器翻译(End-to-End Neural Machine Translation, End-to-End NMT)(见图 1 的右半部分),简称为 NMT 模型。作为经典模型的实现,可以帮助大家更好的理解机器翻译。

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

图 1:基于神经网络的机器翻译系统

效果展示

以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:

这些 是 希望 的 曙光 和 解脱 的 迹象 .

如果设定显示翻译结果的条数为 3,生成的英语句子如下:

0 -5.36816 These are signs of hope and relief . 1 -6.23177 These are the light of hope and relief . 2 -7.7914 These are the light of hope and the relief of hope .

  • 左起第一列是生成句子的序号;左起第二列是该条句子的得分(从大到小),分值越高越好;左起第三列是生成的英语句子。
  • 另外有两个特殊标志: 表示句子的结尾, 表示未登录词(unknown word),即未在训练字典中出现的词。

模型概览

本节依次介绍双向循环神经网络(Bi-directional Recurrent Neural Network),NMT 模型中典型的编码器 - 解码器(Encoder-Decoder)框架以及柱搜索(beam search)算法。

双向循环神经网络

我们这里介绍 Bengio 团队在论文 [ 2,4 ] 中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。

具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步 RNN 的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向 RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(W_1,W_3),隐层到隐层自己的权重矩阵(W_2,W_5),前向隐层和后向隐层到输出层的权重矩阵(W_4,W_6)。注意,该网络的前向隐层和后向隐层之间没有连接。

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

图 2:按时间步展开的双向循环神经网络

编码器 - 解码器框架

编码器 - 解码器(Encoder-Decoder)[2] 框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用 RNN 实现。

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

图 3:编码器 - 解码器框架

编码器

编码阶段分为三步:

  1. one-hot vector 表示:将源语言句子 x={x_1, x_2,…, x_t}的每个词 x_i 表示成一个列向量 W_iϵ〖{0,1}〗^(|v|),i=1,2,…,T。这个向量 W_i 的维度与词汇表大小|V| 相同,并且只有一个维度上有值 1(该位置对应该词在词汇表中的位置),其余全是 0。

  2. 映射到低维语义空间的词向量:one-hot vector 表示存在两个问题,1)生成的向量维度往往很大,容易造成维数灾难;2)难以刻画词与词之间的关系(如语义相似性,也就是无法很好地表达语义)。因此,需再 one-hot vector 映射到低维的语义空间,由一个固定维度的稠密向量(称为词向量)表示。记映射矩阵为 CϵR^(K*|V|),用 S_i= C_(W_i ) 表示第 i 个词的词向量,K 为向量维度。

  3. 用 RNN 编码源语言词序列:这一过程的计算公式为 h_i = ∅_θ (h_(i-1), S_i),其中 h_0 是一个全零的向量,∅_θ是一个非线性激活函数,最后得到的 h = {h_1,…, h_T}就是 RNN 依次读入源语言 T 个词的状态编码序列。整句话的向量表示可以采用 h 在最后一个时间步 T 的状态编码,或使用时间维上的池化(pooling)结果。

第 3 步也可以使用双向循环神经网络实现更复杂的句编码表示,具体可以用双向 GRU 实现。前向 GRU 按照词序列 (x_1,…, x_T) 的顺序依次编码源语言端词,并得到一系列隐层状态 基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列 类似的,后向 GRU 按照 (x_T,…, x_1) 的顺序依次编码源语言端词,得到 基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列 最后对于词 x_i,通过拼接两个 GRU 的结果得到它的隐层状态,即 h_i = 基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

图 4:使用双向 GRU 的编码器

解码器

机器翻译任务的训练过程中,解码阶段的目标是最大化下一个正确的目标语言词的概率。思路是: 1. 每一个时刻,根据源语言句子的编码信息(又叫上下文向量,context vector)c、真实目标语言序列的第 i 个词 u_i 和 i 时刻 RNN 的隐层状态 z_i,计算出下一个隐层状态 z_(i+1)。计算公式如下:

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

其中∅_θ’是一个非线性激活函数;c 是源语言句子的上下文向量,在不使用注意力机制时,如果编码器的输出是源语言句子编码后的最后一个元素,则可以定义 c = h_t ;u_i 是目标语言序列的第 i 个单词,u_0 是目标语言序列的开始标记 < s > ,表示解码开始;z_i 是 i 时刻解码 RNN 的隐层状态,z_0 是一个全零的向量。

  1. 将 z_(i+1) 通过 softmax 归一化,得到目标语言序列的第 i+1 个单词的概率分布 p_(i+1)。概率分布公式如下:

基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列

其中 w_(sZ_(i+1) )+b_z 是对每个可能的输出单词进行打分,再 softmax 归一化就可以得到第 i+1 个词的概率 p_(i+1)。

2. 根据 p_(i+1) 和 u_(i+1) 计算代价。

3. 重复步骤 1~23,直到目标语言序列中的所有词处理完毕。

机器翻译任务的生成过程,通俗来讲就是根据预先训练的模型来翻译源语言句子。生成过程中的解码阶段和上述训练过程的有所差异,具体介绍请见柱搜索算法。

柱搜索算法

柱搜索(beam search)是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点,通常用在解空间非常大的系统(如机器翻译、语音识别)中,原因是内存无法装下图或树中所有展开的解。如在机器翻译任务中希望翻译“< s > 你好 ”,就算目标语言字典中只有 3 个词(< s >, , hello),也可能生成无限句话(hello 循环出现的次数不定),为了找到其中较好的翻译结果,我们可采用柱搜索算法。

柱搜索算法使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost)(本教程中,为生成词的 log 概率之和)对节点进行排序,然后仅留下预先确定的个数(文献中通常称为 beam width、beam size、柱宽度等)的节点。只有这些节点会在下一层继续扩展,其他节点就被剪掉了,也就是说保留了质量较高的节点,剪枝了质量较差的节点。因此,搜索所占用的空间和时间大幅减少,但缺点是无法保证一定获得最优解。

使用柱搜索算法的解码阶段,目标是最大化生成序列的概率。思路是:

1. 每一个时刻,根据源语言句子的编码信息 cc、生成的第 ii 个目标语言序列单词 u_i 和 i 时刻 RNN 的隐层状态 z_i,计算出下一个隐层状态 z_(i+1)。

2. 将 z_(i+1) 通过 softmax 归一化,得到目标语言序列的第 i+1 个单词的概率分布 p_(i+1)。

3. 根据 p_(i+1) 采样出单词 u_(i+1)。

4. 重复步骤 1~3,直到获得句子结束标记 或超过句子的最大生成长度为止。

注意:z_(i+1) 和 p_(i+1) 的计算公式同解码器中的一样。且由于生成时的每一步都是通过贪心法实现的,因此并不能保证得到全局最优解。

数据介绍

本教程使用 WMT-14 数据集中的 bitexts(after selection) 作为训练集,dev+test data 作为测试集和生成集。

数据预处理

我们的预处理流程包括两步:

  • 将每个源语言到目标语言的平行语料库文件合并为一个文件:
  • 合并每个 XXX.src 和 XXX.trg 文件为 XXX。
  • XXX 中的第 i 行内容为 XXX.src 中的第 i 行和 XXX.trg 中的第 i 行连接,用’t’分隔。

创建训练数据的“源字典”和“目标字典”。每个字典都有 DICTSIZE 个单词,包括:语料中词频最高的(DICTSIZE - 3)个单词,和 3 个特殊符号 < s >(序列的开始)、 (序列的结束)和 (未登录词)。

示例数据

因为完整的数据集数据量较大,为了验证训练流程,PaddlePaddle 接口 paddle.dataset.wmt14 中默认提供了一个经过预处理的较小规模的数据集。

该数据集有 193319 条训练数据,6003 条测试数据,词典长度为 30000。因为数据规模限制,使用该数据集训练出来的模型效果无法保证。

模型配置说明

下面我们开始根据输入数据的形式配置模型。首先引入所需的库函数以及定义全局变量。

复制代码

from __future__ import print_function
import paddle
import paddle.fluid as fluid
import paddle.fluid.layers as pd
import os
import sys
try:
from paddle.fluid.contrib.trainer import *
from paddle.fluid.contrib.inferencer import *
except ImportError:
print(
"In the fluid 1.0, the trainer and inferencer are moving to paddle.fluid.contrib",
file=sys.stderr)
from paddle.fluid.trainer import *
from paddle.fluid.inferencer import *

dict_size = 30000# 字典维度
source_dict_dim = target_dict_dim = dict_size# 源 / 目标语言字典维度
hidden_dim = 32# 编码器中的隐层大小
word_dim = 16# 词向量维度
batch_size = 2# batch 中的样本数
max_length = 8# 生成句子的最大长度
topk_size = 50
beam_size = 2# 柱宽度

is_sparse = True
decoder_size = hidden_dim# 解码器中的隐层大小
model_save_dir ="machine_translation.inference.model"

然后如下实现编码器框架:

复制代码

def encoder(is_sparse):
# 定义源语言 id 序列的输入数据
src_word_id = pd.data(
name="src_word_id", shape=[1],dtype='int64',lod_level=1)
# 将上述编码映射到低维语言空间的词向量
src_embedding = pd.embedding(
input=src_word_id,
size=[dict_size, word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))
# LSTM 层:fc + dynamic_lstm
fc1 = pd.fc(input=src_embedding,size=hidden_dim * 4,act='tanh')
lstm_hidden0, lstm_0 = pd.dynamic_lstm(input=fc1,size=hidden_dim * 4)
# 取源语言序列编码后的最后一个状态
encoder_out = pd.sequence_last_step(input=lstm_hidden0)
return encoder_out

再实现训练模式下的解码器:

复制代码

def train_decoder(context):
# 定义目标语言 id 序列的输入数据,并映射到低维语言空间的词向量
trg_language_word= pd.data(
name="target_language_word",shape=[1],dtype='int64',lod_level=1)
trg_embedding= pd.embedding(
input=trg_language_word,
size=[dict_size,word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))

rnn= pd.DynamicRNN()
withrnn.block():# 使用 DynamicRNN 定义每一步的计算
# 获取当前步目标语言输入的词向量
current_word= rnn.step_input(trg_embedding)
# 获取隐层状态
pre_state= rnn.memory(init=context)
# 解码器计算单元:单层前馈网络
current_state= pd.fc(input=[current_word,pre_state],
size=decoder_size,
act='tanh')
# 计算归一化的单词预测概率
current_score= pd.fc(input=current_state,
size=target_dict_dim,
act='softmax')
# 更新 RNN 的隐层状态
rnn.update_memory(pre_state, current_state)
# 输出预测概率
rnn.output(current_score)

return rnn()

实现推测模式下的解码器:

复制代码

def decode(context):
init_state= context
# 定义解码过程循环计数变量
array_len= pd.fill_constant(shape=[1],dtype='int64',value=max_length)
counter= pd.zeros(shape=[1],dtype='int64',force_cpu=True)

# 定义 tensor array 用以保存各个时间步的内容,并写入初始 id,score 和 state
state_array= pd.create_array('float32')
pd.array_write(init_state,array=state_array,i=counter)

ids_array= pd.create_array('int64')
scores_array= pd.create_array('float32')

init_ids= pd.data(name="init_ids",shape=[1],dtype="int64",lod_level=2)
init_scores= pd.data(
name="init_scores",shape=[1],dtype="float32",lod_level=2)

pd.array_write(init_ids,array=ids_array,i=counter)
pd.array_write(init_scores,array=scores_array,i=counter)

# 定义循环终止条件变量
cond= pd.less_than(x=counter,y=array_len)
# 定义 while_op
while_op= pd.While(cond=cond)
withwhile_op.block():# 定义每一步的计算
# 获取解码器在当前步的输入,包括上一步选择的 id,对应的 score 和上一步的 state
pre_ids= pd.array_read(array=ids_array,i=counter)
pre_state= pd.array_read(array=state_array,i=counter)
pre_score= pd.array_read(array=scores_array,i=counter)

# 更新输入的 state 为上一步选择 id 对应的 state
pre_state_expanded= pd.sequence_expand(pre_state, pre_score)
# 同训练模式下解码器中的计算逻辑,包括获取输入向量,解码器计算单元计算和
# 归一化单词预测概率的计算
pre_ids_emb= pd.embedding(
input=pre_ids,
size=[dict_size,word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))

current_state= pd.fc(input=[pre_state_expanded,pre_ids_emb],
size=decoder_size,
act='tanh')
current_state_with_lod= pd.lod_reset(x=current_state,y=pre_score)
current_score= pd.fc(input=current_state_with_lod,
size=target_dict_dim,
act='softmax')
topk_scores,topk_indices= pd.topk(current_score,k=beam_size)

# 计算累计得分,进行 beam search
accu_scores= pd.elementwise_add(
x=pd.log(topk_scores),y=pd.reshape(pre_score,shape=[-1]),axis=0)
selected_ids,selected_scores= pd.beam_search(
pre_ids,
pre_score,
topk_indices,
accu_scores,
beam_size,
end_id=10,
level=0)

withpd.Switch() as switch:
withswitch.case(pd.is_empty(selected_ids)):
pd.fill_constant(
shape=[1],value=0,dtype='bool',force_cpu=True,out=cond)
withswitch.default():
pd.increment(x=counter,value=1,in_place=True)

pd.array_write(current_state,array=state_array,i=counter)
pd.array_write(selected_ids,array=ids_array,i=counter)
pd.array_write(selected_scores,array=scores_array,i=counter)

length_cond= pd.less_than(x=counter,y=array_len)
finish_cond= pd.logical_not(pd.is_empty(x=selected_ids))
pd.logical_and(x=length_cond,y=finish_cond,out=cond)

translation_ids,translation_scores= pd.beam_search_decode(
ids=ids_array,scores=scores_array,beam_size=beam_size,end_id=10)

return translatio
n_ids, translation_scores

进而,我们定义一个 train_program 来使用 inference_program 计算出的结果,在标记数据的帮助下来计算误差。我们还定义了一个 optimizer_func 来定义优化器。

复制代码

def train_program():
context = encoder()
rnn_out = train_decoder(context)
label = pd.data(
name="target_language_next_word", shape=[1],dtype='int64',lod_level=1)
cost = pd.cross_entropy(input=rnn_out,label=label)
avg_cost = pd.mean(cost)
return avg_cost

def optimizer_func():
return fluid.optimizer.Adagrad(
learning_rate=1e-4,
regularization=fluid.regularizer.L2DecayRegularizer(
regularization_coeff=0.1))

训练模型

定义训练环境

定义您的训练环境,可以指定训练是发生在 CPU 还是 GPU 上。

复制代码

ifuse_cudaandnotfluid.core.is_compiled_with_cuda():
return
plac
e = fluid.CUDAPlace(0)ifuse_cudaelsefluid.CPUPlace()

定义数据提供器

下一步是为训练和测试定义数据提供器。提供器读入一个大小为 BATCH_SIZE 的数据。paddle.dataset.wmt.train 每次会在乱序化后提供一个大小为 BATCH_SIZE 的数据,乱序化的大小为缓存大小 buf_size。

复制代码

train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.wmt14.train(dict_size), buf_size=1000),
batch_size=batch_size)

构造训练器 (trainer)

训练器需要一个训练程序和一个训练优化函数。

复制代码

trainer = Trainer(
train_func=train_program,place=place,optimizer_func=optimizer_func)

提供数据

feed_order 用来定义每条产生的数据和 paddle.layer.data 之间的映射关系。比如,wmt14.train 产生的第一列的数据对应的是 src_word_id 这个特征。

复制代码

feed_order= [
'src_word_id','target_language_word','target_language_next_word'
]

事件处理器

回调函数 event_handler 在一个之前定义好的事件发生后会被调用。例如,我们可以在每步训练结束后查看误差。

复制代码

defevent_handler(event):
ifisinstance(event, EndStepEvent):
ifevent.step % 10 ==0:
print('pass_id='+ str(event.epoch) +' batch='+ str(event.step))
ifisinstance(event, EndEpochEvent):
trainer.save_params(model_save_dir)
{1}

开始训练

最后,我们传入训练循环数(num_epoch)和一些别的参数,调用 trainer.train 来开始训练。

复制代码

trainer = Trainer(
train_func=train_program,place=place,optimizer_func=optimizer_func)
trainer.train(
reader=train_reader,
num_epochs=EPOCH_NUM,
event_handler=event_handler,
feed_order=feed_order)

应用模型

定义解码部分

使用上面定义的 encoder 和 decoder 函数来推测翻译后的对应 id 和分数.

复制代码

context = encoder()
translation_ids, translation_scores = decode(context)

定义数据

我们先初始化 id 和分数来生成 tensors 来作为输入数据。在这个预测例子中,我们用 wmt14.test 数据中的第一个记录来做推测,最后我们用 " 源字典 " 和 " 目标字典 " 来列印对应的句子结果。

复制代码

init_ids_data = np.array([1 for _ in range(batch_size)], dtype='int64')
init_scores_data = np.array(
[1. for _ in range(batch_size)], dtype='float32')
init_ids_data = init_ids_data.reshape((batch_size, 1))
init_scores_data = init_scores_data.reshape((batch_size, 1))
init_lod = [1] * batch_size
init_lod = [init_lod, init_lod]

init_ids = fluid.create_lod_tensor(init_ids_data, init_lod, place)
init_scores = fluid.create_lod_tensor(init_scores_data, init_lod, place)

test_data = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.wmt14.test(dict_size), buf_size=1000),
batch_size=batch_size)

feed_order = ['src_word_id']
feed_list = [
framework.default_main_program().global_block().var(var_name)
for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list, place)

src_dict, trg_dict = paddle.dataset.wmt14.get_dict(dict_size)

测试

现在我们可以进行预测了。我们要在 feed_order 提供对应参数,放在 executor 上运行以取得 id 和分数结果

复制代码

for data in test_data():
feed_data = map(lambda x: [x[0]], data)
feed_dict = feeder.feed(feed_data)
feed_dict['init_ids'] = init_ids
feed_dict['init_scores'] = init_scores

results = exe.run(
framework.default_main_program(),
feed=feed_dict,
fetch_list=[translation_ids, translation_scores],
return_numpy=False)

result_ids = np.array(results[0])
result_ids_lod = results[0].lod()
result_scores = np.array(results[1])

print("Original sentence:")
print(" ".join([src_dict[w] for w in feed_data[0][0][1:-1]]))
print("Translated score and sentence:")
for i in xrange(beam_size):
start_pos = result_ids_lod[1][i] +1
end_pos = result_ids_lod[1][i+1]
print("%d\t%.4f\t%s\n"% (i+1, result_scores[end_pos-1],
" ".join([trg_dict[w] for w in result_ids[start_pos:end_pos]])))

break

总结

端到端的神经网络机器翻译是近几年兴起的一种全新的机器翻译方法。在本文中,我们介绍了 NMT 中典型的“编码器 - 解码器”框架。由于 NMT 是一个典型的 Seq2Seq(Sequence to Sequence,序列到序列)学习问题,因此,Seq2Seq 中的 query 改写(query rewriting)、摘要、单轮对话等问题都可以用本教程的模型来解决。

参考文献

1.Koehn P. Statistical machine translation[M]. Cambridge University Press, 2009.

2. Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoder for statistical machine translation[C] //Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), 2014: 1724-1734.

3. Chung J, Gulcehre C, Cho K H, et al. Empirical evaluation of gated recurrent neural networks on sequence modeling [J]. arXiv preprint arXiv:1412.3555, 2014.

4. Bahdanau D, Cho K, Bengio Y. Neural machine translation by jointly learning to align and translate [C]//Proceedings of ICLR 2015, 2015.

5. Papineni K, Roukos S, Ward T, et al. BLEU: a method for automatic evaluation of machine translation [C]//Proceedings of the 40th annual meeting on association for computational linguistics. Association for Computational Linguistics, 2002: 311-318.


以上所述就是小编给大家介绍的《基于 PaddlePaddle 的机器翻译教程 | 深度学习基础任务系列》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员的英语

程序员的英语

[韩]朴栽浒、[韩]李海永 / 颜廷连 / 人民邮电出版社 / 2018-2 / 49.00元

高考以后就把英语都还给老师了? 写代码特顺溜,一到英语就卡壳? 常见的语法书太枯燥,单词书又太宽泛? 不用急,快来加入针对开发人员的英语读解能力训练项目! - 安全与黑客攻击、无人机与机器人、大数据、物联网、云计算,顺应新技术潮流! - 语法、单词、完形填空、阅读理解、翻译,多角度提升读解能力! - 英语母语技术人员审校,提供“语言和技术”双保险!一起来看看 《程序员的英语》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具