【论文泛读】4. 机器翻译:Neural Machine Translation by Jointly Learning to Align and Translate

本文详细介绍了基于注意力机制的神经机器翻译模型的实现过程,涵盖了数据预处理、模型搭建、训练和测试的全部步骤。包括词汇表的构建、编码器-解码器结构、动态RNN的运用以及填充和批处理技术。此外,还分享了Python中的实用技巧,如使用collections.Counter统计词频、使用tf.data.Dataset进行数据处理等。
摘要由CSDN通过智能技术生成

更新进度:■■■■■■■■■■■■■■■■■■■■■■■|100%
理论上一周更一个经典论文
刚刚开始学习,写的不好,有错误麻烦大家留言给我啦
这位博主的笔记短小精炼,爱了爱了:点击跳转

准备

文章标题:
Neural Machine Translation by Jointly Learning to Align and Translate
基于联合学习对齐和翻译的神经机器翻译
作者:
Dzmitry Bahdanau1, KyungHyun Cho2, Yoshua Bengio*2
单位:

  1. 不来梅雅克布大学Jacobs University Bremen, Germany
  2. 蒙特利尔大学Universite de Montr ´ eal

发表会议及时间:
ICLR 2015综合性会议
最早将注意力机制引入机器翻译的论文

理论知识

在这里插入图片描述
模型:
在这里插入图片描述
在这里插入图片描述

代码实现(英文翻中文)

数据集:IWLST TED数据集、PTB数据集

1. 数据预处理

在这里插入图片描述

step 1,2: 分词,统计

import codecs
import collections

counter = collections.Counter()  # 建立一个空的Counter
with codecs.open(RAW_DATA, "r", "utf-8") as f:  # RAW_DATA 是 ptb.train.txt
    for line in f:
        for word in line.strip().split():
            counter[word] += 1

输出:按词频排列的词典
在这里插入图片描述

  1. collections.Counter()是Python的计数器,快速的统计一个字典里面每个元素出现的次数,参数必须是list类型的,不能是二维的,或者其他维度的。
    可以创建一个空的Counter,对空的Counter进行操作。
    参考文章
  2. line.strip().split():
    strip()表示删除掉数据中的换行符,split()表示数据中遇到空格就隔开。
  3. 字典(Dictionary).items() 函数以列表(list)形式返回可遍历的(键, 值) 元组数组。
  4. 列表的排列

例:list1 = [(3, 8, 2), (1, 5, 8), (2, 5, 7)]

方法1:利用list自带sort()方法
list1.sort(key=lambda t: t[0])
print(list1) # [(1, 5, 8), (2, 5, 7), (3, 8, 2)]

方法2:利用集合公共函数sorted()–>排序后的集合
ret = sorted(list1, key=lambda t: t[0])
print(ret) # [(1, 5, 8), (2, 5, 7), (3, 8, 2)]

step 3: 分配ID

# 按词频顺序对单词进行排序。
sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True)
sorted_words = [x[0] for x in sorted_word_to_cnt]

在这里插入图片描述

# 稍后我们需要在文本换行处加入句子结束符"<eos>",这里预先将其加入词汇表。
sorted_words = ["<eos>"] + sorted_words

在这里插入图片描述
输出:词汇vocabe

with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output:
    for word in sorted_words:
        file_output.write(word + "\n")

step 4: 生成词汇表

with codecs.open("./ptb.vocab", "r", "utf-8") as f_vocab:
    vocab = [w.strip() for w in f_vocab.readlines()]
word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))}

将词汇频率对应vocab
在这里插入图片描述
输出:生成词汇表(字典)
在这里插入图片描述
如果出现了不在词汇表内的低频词,则替换为"unk"

def get_id(word):
    return word_to_id[word] if word in word_to_id else word_to_id["<unk>"]
  1. zip(x,y)

x = [1, 2, 3]
y = [4, 5, 6]
zipped = zip(x, y)

list(zipped)
[(1, 4), (2, 5), (3, 6)]

step 5: 编号表示

fin = codecs.open(RAW_DATA, "r", "utf-8")
fout = codecs.open(OUTPUT_DATA, 'w', 'utf-8')

for line in fin:
    words = line.strip().split() + ["<eos>"]  # 读取单词并添加<eos>结束符
    # 将每个单词替换为词汇表中的编号
    out_line = ' '.join([str(get_id(w)) for w in words]) + '\n'
    fout.write(out_line)

在这里插入图片描述

step 6: 预料填充(分批次处理,每一批次语句长度一样)

输入:已经用空格隔开并转换为编码的数据集

def MakeDataset(file_path):
    dataset = tf.data.TextLineDataset(file_path)
    # 根据空格将单词编号切分开并放入一个一维向量。
    dataset = dataset.map(lambda string: tf.string_split([string]).values)
    # 将字符串形式的单词编号转化为整数。
    dataset = dataset.map(lambda string: tf.string_to_number(string, tf.int32))
    # 统计每个句子的单词数量,并与句子内容一起放入Dataset中。
    dataset = dataset.map(lambda x: (x, tf.size(x)))
    return dataset

输出:单词编号转换为整数,补充了句子长度的数据集

# 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和
# batching操作。
def MakeSrcTrgDataset(src_path, trg_path, batch_size):
    # 首先分别读取源语言数据和目标语言数据。
    src_data = MakeDataset(src_path)  # SRC_TRAIN_DATA = "./train.en"  
    trg_data = MakeDataset(trg_path)  # TRG_TRAIN_DATA = "./train.zh"
    # 通过zip操作将两个Dataset合并为一个Dataset。现在每个Dataset中每一项数据ds
    # 由4个张量组成:
    #   ds[0][0]是源句子
    #   ds[0][1]是源句子长度
    #   ds[1][0]是目标句子
    #   ds[1][1]是目标句子长度
    dataset = tf.data.Dataset.zip((src_data, trg_data))

    # 删除内容为空(只包含<EOS>,句子结束符)的句子和长度过长的句子。
    def FilterLength(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        src_len_ok = tf.logical_and(tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN))
        trg_len_ok = tf.logical_and(tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
        return tf.logical_and(src_len_ok, trg_len_ok)
    dataset = dataset.filter(FilterLength)
    
    # 从图9-5可知,解码器需要两种格式的目标句子:
    #   1.解码器的输入(trg_input),形式如同"<sos> X Y Z"
    #   2.解码器的目标输出(trg_label),形式如同"X Y Z <eos>"
    # 上面从文件中读到的目标句子是"X Y Z <eos>"的形式,我们需要从中生成"<sos> X Y Z"
    # 形式并加入到Dataset中。

    def MakeTrgInput(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
        return ((src_input, src_len), (trg_input, trg_label, trg_len))
    dataset = dataset.map(MakeTrgInput)

    # 随机打乱训练数据。
    dataset = dataset.shuffle(10000)

    # 规定填充后输出的数据维度。
    padded_shapes = (
        (tf.TensorShape([None]),      # 源句子是长度未知的向量
         tf.TensorShape([])),         # 源句子长度是单个数字
        (tf.TensorShape([None]),      # 目标句子(解码器输入)是长度未知的向量
         tf.TensorShape([None]),      # 目标句子(解码器目标输出)是长度未知的向量
         tf.TensorShape([])))         # 目标句子长度是单个数字
    # 调用padded_batch方法进行batching操作。
    batched_dataset = dataset.padded_batch(batch_size, padded_shapes)  # 填充句子到这一轮训练句子的最长长度,让每个句子同等长度
    return batched_dataset
  1. 填充调用函数为:tf. data. Dataset.padded_batch(batch_size, padded_shapes)
  2. lambda+map函数的使用

第一个问题: lambda如何用?
lambda叫做匿名函数:不需要提前定义函数就可以使用的函数
如果不使用lambda函数:
输入:

def func(x):
… return x+1

输出:

func(1)

使用lambda函数:
输入:

a = lambda x:x+1

输出:

a(1)

第二个问题: map如何用?
例:有一个元组列表[(‘a’,1),(‘b’,2),(‘c’,3),(‘d’,4)],需将字母项提取为新的列表。
vartuple = [(‘a’,1),(‘b’,2),(‘c’,3),(‘d’,4)]
list(map(lambda x:x[0],vartuple))
输出:[‘a’, ‘b’, ‘c’, ‘d’]

2. 模型搭建

熟悉模型流程+输入输出

# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
           for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(NUM_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])  # 源语言词向量
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])  # 目标语言词向量

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
           self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
           self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    # 在forward函数中定义模型的前向计算图。
    # src_input, src_size, trg_input, trg_label, trg_size分别是上面
    # MakeSrcTrgDataset函数产生的五种张量。
    def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
        batch_size = tf.shape(src_input)[0]
    
        # 将输入和输出单词编号转为词向量。
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)
        trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)
        
        # 在词向量上进行dropout。
        src_emb = tf.nn.dropout(src_emb, KEEP_PROB)
        trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB)

        # 使用dynamic_rnn构造编码器。
        # 编码器读取源句子每个位置的词向量,输出最后一步的隐藏状态enc_state。
        # 因为编码器是一个双层LSTM,因此enc_state是一个包含两个LSTMStateTuple类
        # 张量的tuple,每个LSTMStateTuple对应编码器中的一层。
        # enc_outputs是顶层LSTM在每一步的输出,它的维度是[batch_size, 
        # max_time, HIDDEN_SIZE]。Seq2Seq模型中不需要用到enc_outputs,而
        # 后面介绍的attention模型会用到它。
        with tf.variable_scope("encoder"):
            enc_outputs, enc_state = tf.nn.dynamic_rnn(
                self.enc_cell, src_emb, src_size, dtype=tf.float32)

        # 使用dyanmic_rnn构造解码器。
        # 解码器读取目标句子每个位置的词向量,输出的dec_outputs为每一步
        # 顶层LSTM的输出。dec_outputs的维度是 [batch_size, max_time,
        # HIDDEN_SIZE]。
        # initial_state=enc_state表示用编码器的输出来初始化第一步的隐藏状态。
        with tf.variable_scope("decoder"):
            dec_outputs, _ = tf.nn.dynamic_rnn(
                self.dec_cell, trg_emb, trg_size, initial_state=enc_state)

        # 计算解码器每一步的log perplexity。这一步与语言模型代码相同。
        output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
        logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
            labels=tf.reshape(trg_label, [-1]), logits=logits)

        # 在计算平均损失时,需要将填充位置的权重设置为0,以避免无效位置的预测干扰
        # 模型的训练。
        label_weights = tf.sequence_mask(trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32)
        label_weights = tf.reshape(label_weights, [-1])
        cost = tf.reduce_sum(loss * label_weights)
        cost_per_token = cost / tf.reduce_sum(label_weights)
        
        # 定义反向传播操作。反向操作的实现与语言模型代码相同。
        trainable_variables = tf.trainable_variables()

        # 控制梯度大小,定义优化方法和训练步骤。
        grads     = tf.gradients(cost / tf.to_float(batch_size),trainable_variables)
        grads, _  = tf.clip_by_global_norm(grads, MAX_GRAD_NORM)
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        train_op  = optimizer.apply_gradients(zip(grads, trainable_variables))
        return cost_per_token, train_op

# 使用给定的模型model上训练一个epoch,并返回全局步数。
# 每训练200步便保存一个checkpoint。
def run_epoch(session, cost_op, train_op, saver, step):
    # 训练一个epoch。
    # 重复训练步骤直至遍历完Dataset中所有数据。
    while True:
        try:
            # 运行train_op并计算损失值。训练数据在main()函数中以Dataset方式提供。
            cost, _ = session.run([cost_op, train_op])
            if step % 10 == 0:
                print("After %d steps, per token cost is %.3f" % (step, cost))
            # 每200步保存一个checkpoint。
            if step % 200 == 0:
                saver.save(session, CHECKPOINT_PATH, global_step=step)
            step += 1
        except tf.errors.OutOfRangeError:
            break
    return step

3. 训练


'''
训练步骤:
1.初始化
2.定义模型
3.定义输入数据
4.定义前向图
5.训练模型
'''


def main():
    # 定义初始化函数。
    initializer = tf.random_uniform_initializer(-0.05, 0.05)  # 均匀分布

    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None, initializer=initializer):
        train_model = NMTModel()
  
    # 定义输入数据。
    data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE)
    iterator = data.make_initializable_iterator()
    (src, src_size), (trg_input, trg_label, trg_size) = iterator.get_next()
 
    # 定义前向计算图。输入数据以张量形式提供给forward函数。
    cost_op, train_op = train_model.forward(src, src_size, trg_input, trg_label, trg_size)

    # 训练模型。
    saver = tf.train.Saver()
    step = 0
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(NUM_EPOCH):
            print("In iteration: %d" % (i + 1))
            sess.run(iterator.initializer)
            step = run_epoch(sess, cost_op, train_op, saver, step)

4. 测试

import tensorflow as tf
import codecs
import sys

# 读取checkpoint的路径。9000表示是训练程序在第9000步保存的checkpoint。
CHECKPOINT_PATH = "./seq2seq_ckpt-200"

# 模型参数。必须与训练时的模型参数保持一致。
HIDDEN_SIZE = 1024                         	# LSTM的隐藏层规模。
NUM_LAYERS = 2                             	# 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000                   	# 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                    	# 目标语言词汇表大小。
SHARE_EMB_AND_SOFTMAX = True            	# 在Softmax层和词向量层之间共享参数。

# 词汇表文件
SRC_VOCAB = "./en.vocab"
TRG_VOCAB = "./zh.vocab"

# 词汇表中<sos>和<eos>的ID。在解码过程中需要用<sos>作为第一步的输入,并将检查
# 是否是<eos>,因此需要知道这两个符号的ID。
SOS_ID = 1
EOS_ID = 2

# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
           for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(NUM_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
            self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
            self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    def inference(self, src_input):
        # 虽然输入只有一个句子,但因为dynamic_rnn要求输入是batch的形式,因此这里
        # 将输入句子整理为大小为1的batch。
        src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32)
        src_input = tf.convert_to_tensor([src_input], dtype=tf.int32)
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)

        # 使用dynamic_rnn构造编码器。这一步与训练时相同。
        with tf.variable_scope("encoder"):
            enc_outputs, enc_state = tf.nn.dynamic_rnn(
                self.enc_cell, src_emb, src_size, dtype=tf.float32)
   
        # 设置解码的最大步数。这是为了避免在极端情况出现无限循环的问题。
        MAX_DEC_LEN=100

        with tf.variable_scope("decoder/rnn/multi_rnn_cell"):
            # 使用一个变长的TensorArray来存储生成的句子。
            init_array = tf.TensorArray(dtype=tf.int32, size=0,
                dynamic_size = True, clear_after_read=False)
            # 填入第一个单词<sos>作为解码器的输入。
            init_array = init_array.write(0, SOS_ID)
            # 构建初始的循环状态。循环状态包含循环神经网络的隐藏状态,保存生成句子的
            # TensorArray,以及记录解码步数的一个整数step。
            init_loop_var = (enc_state, init_array, 0)

            # tf.while_loop的循环条件:
            # 循环直到解码器输出<eos>,或者达到最大步数为止。
            def continue_loop_condition(state, trg_ids, step):
                return tf.reduce_all(tf.logical_and(
                    tf.not_equal(trg_ids.read(step), EOS_ID),
                    tf.less(step, MAX_DEC_LEN-1)))

            def loop_body(state, trg_ids, step):
                # 读取最后一步输出的单词,并读取其词向量。
                trg_input = [trg_ids.read(step)]
                trg_emb = tf.nn.embedding_lookup(self.trg_embedding,
                                                 trg_input)
                # 这里不使用dynamic_rnn,而是直接调用dec_cell向前计算一步。
                dec_outputs, next_state = self.dec_cell.call(
                    state=state, inputs=trg_emb)
                # 计算每个可能的输出单词对应的logit,并选取logit值最大的单词作为
                # 这一步的而输出。
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight)
                          + self.softmax_bias)
                next_id = tf.argmax(logits, axis=1, output_type=tf.int32)
                # 将这一步输出的单词写入循环状态的trg_ids中。
                trg_ids = trg_ids.write(step+1, next_id[0])
                return next_state, trg_ids, step+1

            # 执行tf.while_loop,返回最终状态。
            state, trg_ids, step = tf.while_loop(
                continue_loop_condition, loop_body, init_loop_var)
            return trg_ids.stack()

def main():
    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None):
        model = NMTModel()

    # 定义个测试句子。
    test_en_text = "This is a test . <eos>"
    print(test_en_text)
    
    # 根据英文词汇表,将测试句子转为单词ID。
    with codecs.open(SRC_VOCAB, "r", "utf-8") as f_vocab:
        src_vocab = [w.strip() for w in f_vocab.readlines()]
        src_id_dict = dict((src_vocab[x], x) for x in range(len(src_vocab)))
    test_en_ids = [(src_id_dict[token] if token in src_id_dict else src_id_dict['<unk>'])
                   for token in test_en_text.split()]
    print(test_en_ids)

    # 建立解码所需的计算图。
    output_op = model.inference(test_en_ids)
    sess = tf.Session()
    saver = tf.train.Saver()
    saver.restore(sess, CHECKPOINT_PATH)

    # 读取翻译结果。
    output_ids = sess.run(output_op)
    print(output_ids)
    
    # 根据中文词汇表,将翻译结果转换为中文文字。
    with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab:
        trg_vocab = [w.strip() for w in f_vocab.readlines()]
    output_text = ''.join([trg_vocab[x] for x in output_ids])
    
    # 输出翻译结果。
    print(output_text.encode('utf8').decode(sys.stdout.encoding))
    sess.close()

if __name__ == "__main__":
    main()

代码可学习技巧

  1. checkpoint保存每一步权重文件
  2. 使用argparse.ArgumentParser(),argparse 模块可以让人轻松编写用户友好的命令行接口,将变量通过命令行赋值
第一章 流体力学概述 1、Introduction to Fluid Mechanics流体力学简介 2、Governing Equations of Fluid Mechanics流体力学控制方程 3、Potential Flow位流 4、Boundary Layer and Viscous Flows边界层与粘性流 5、External Flows外流 6、Internal Flows内流 7、Heat Transfer传热学 第二章 CFD概述 1、Introduction to CFD 计算流体力学简介 2、Mathematical Equations of CFD计算流体力学的数学方程 3、Overview of CFD Solution Methodologies计算流体力学求解方法 4、Discretization 方程离散 5、Solution Methods 求解方法 6、Postprocessing and Visualization后处理与可视化 7、Modeling Turbulent Flows湍流模拟 8、Designing Meshes for CFD Calculations网格设计 9、Case Studies算例学习 第三章 Gambit几何建模 1、Introduction to GAMBIT + Demo Gambit简介 2、Tutorial – Creating and Meshing a Basic Geometry基本几何的建立与网格生成 3、Introduction to Geometry Operations 几何建模简介 4、Exercise 1练习1 5、CAD/CAE Data Import and Geometry Cleanup CAD/CAE数据导入与几何简化 6、Tutorial 算例 7、Semi-Automated Cleanup Tools + Demo半自动几何简化工具 8、Tutorial 算例 第四章 Gambit网格生成 1、Edge and Face Meshing 边与面的网格生成 2、Exercise 2 – 2D Geometry and Mesh Generation二维几何与网格生成 3、Volume Meshing 体网格生成 4、Exercise 3 – 3D Geometry and Mesh Generation三维几何与网格生成 5、Mesh Control & Size Functions网格控制与SF 6、Exercise 4 算例 7、Meshing Strategy & Tips网格生成策略与技巧 8、Tutorial 算例 第五章 TGrid网格生成简介 1、面网格修正 2、边界层网格生成 3、体网格生成 第六章 FLUENT解算器 1、Introduction to ANSYS—Fluent 简介 2、Lecture – Introduction to CFD Analysis CFD分析简介 3、Demonstration: Overview of the CFD Process CFD流程 4、Tutorial Session – Tutorial #1 (3D Mixing Elbow) 算例 5、Lecture – Solver Basics 解算器基础 6、Lecture – Boundary Conditions边界条件 7、Tutorial Session 算例 8、Lecture – Solver Settings 解算器设置 9、Tutorial Session 算例 10、Lecture – Turbulence Modeling 湍流模拟 11、Tutorial Session 算例 12、Lecture – Heat Transfer Modeling 传热模拟 13、Lecture – User Defined Functions 用户自定义UDF 14、Lecture (Optional) –Multiphase Modeling 多相流简模型 15、Lecture (Optional) – Moving Zone Models 运动区域模拟 16、Tutorial Session 算例 17、案例分析
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

浪里摸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值
>