【课程总结】Day18:Seq2Seq的深入了解

news2024/11/15 21:21:54

前言

在上一章【课程总结】Day17(下):初始Seq2Seq模型中,我们初步了解了Seq2Seq模型的基本情况及代码运行效果,本章内容将深入了解Seq2Seq模型的代码,梳理代码的框架图、各部分组成部分以及运行流程。

框架图

工程目录结构

查看项目目录结构如下:

seq2seq_demo/
├── data.txt                     # 原始数据文件,包含训练或测试数据
├── dataloader.py                # 数据加载器,负责读取和预处理数据
├── decoder.py                   # 解码器实现,用于生成输出序列
├── encoder.py                   # 编码器实现,将输入序列编码为上下文向量
├── main.py                      # 主程序入口,执行模型训练和推理
├── seq2seq.py                   # seq2seq 模型的实现,整合编码器和解码器
└── tokenizer.py                 # 分词器实现,将文本转换为模型可处理的格式

查看各个py文件整理关系图结构如下:

  • main.py 文件是主程序入口,同时其中也定义了 Translation类,用于训练和推理。
  • Translation类在 __init__() 方法中调用 get_tokenizer() 方法实例化tokenizer对象。
  • Translation类在 __init__() 方法中调用 get_model() 实例化seq2seq类对象,进而实例化 EncoderDecoder 对象。
  • Translation类在 train() 方法中调用 get_dataloader() 方法实例化dataloader对象。

核心逻辑

初始化过程

  • 上述流程中较为重要的代码主要是 build_dict() 、encoder实例化、decoder实例化初始化过程:
Build_dict()
def build_dict(self):
        """
        构建字典
        """
        if os.path.exists(self.saved_dict):
            self.load()
            print("加载本地字典成功")
            return

        input_words = {"<UNK>", "<PAD>"}
        output_words = {"<UNK>", "<PAD>", "<SOS>", "<EOS>"}

        with open(file=self.data_file, mode="r", encoding="utf8") as f:
            for line in tqdm(f.readlines()):
                if line:
                    input_sentence, output_sentence = line.strip().split("\t")
                    input_sentence_words = self.split_input(input_sentence)
                    output_sentence_words = self.split_output(output_sentence)
                    input_words = input_words.union(set(input_sentence_words))
                    output_words = output_words.union(set(output_sentence_words))
        # 输入字典
        self.input_word2idx = {word: idx for idx, word in enumerate(input_words)}
        self.input_idx2word = {idx: word for word, idx in self.input_word2idx.items()}
        self.input_dict_len = len(self.input_word2idx)

        # 输出字典
        self.output_word2idx = {word: idx for idx, word in enumerate(output_words)}
        self.output_idx2word = {idx: word for word, idx in self.output_word2idx.items()}
        self.output_dict_len = len(self.output_word2idx)

        # 保存
        self.save()
        print("保存字典成功")

代码解析:

  • 首先,判断本地是否有字典,有的话直接加载;
  • 其次,在input_wordsoutput_words 集合中添加特殊符号(special tokens):
    • <UNK>:表示未知单词,用于表示输入序列中未在字典中找到的单词;
    • <PAD>:表示填充符号,用于填充输入序列和输出序列,使它们具有相同的长度;
    • <SOS>:表示序列的开始,用于表示输出序列的起始位置;
    • <EOS>:表示序列的结束,用于表示输出序列的结束位置。
  • 然后,读取data.txt文件,以\t切分数据并切分单词:
    • 输入的英文调用split_input进行预处理,例如:I’m a student.→[‘i’, ‘m’, ‘a’, ‘student’, ‘.’]
    • 输出的中文调用split_output进行切分,例如:我爱北京天安门→[‘我’, ‘爱’, ‘北京’, ‘天安门’]
  • 最后,调用self.save() 方法将字典保存到本地文件 self.saved_dict 中。
encoder
import torch
from torch import nn


class Encoder(nn.Module):
    """
        定义一个 编码器
    """

    def __init__(self, tokenizer):
        super(Encoder, self).__init__()
        self.tokenizer = tokenizer
        # 嵌入层
        self.embed = nn.Embedding(num_embeddings=self.tokenizer.input_dict_len,
                                  embedding_dim=self.tokenizer.input_embed_dim,
                                  padding_idx=self.tokenizer.input_word2idx.get("<PAD>"))
        # GRU单元
        self.gru = nn.GRU(input_size=self.tokenizer.input_embed_dim,
                          hidden_size=self.tokenizer.input_hidden_size,
                          batch_first=False)
    
    def forward(self, x, x_len):
        # [seq_len, batch_size] --> [seq_len, batch_size, embed_dim]
        x = self.embed(x)
        # 压紧被填充的序列
        x = nn.utils.rnn.pack_padded_sequence(input=x,
                                              lengths=x_len,
                                              batch_first=False)
        out, hn = self.gru(x)
        # 填充被压紧的序列
        out, out_len = nn.utils.rnn.pad_packed_sequence(sequence=out,
                                                        batch_first=False,
                                                        padding_value=self.tokenizer.input_word2idx.get("<PAD>"))
        # out: [seq_len, batch_size, hidden_size]
        # hn: [1, batch_size, hidden_size]
        return out, hn

代码解析:

  • encoder是一个典型的RNN结构,其定义了embedding层用于词嵌入,以及GRU单元进行序列处理。
  • forward方法中,首先将输入序列进行词嵌入,然后使用pack_padded_sequence将被填充的序列压紧,以便于GRU单元处理。
decoder
import torch
from torch import nn
import random

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


class Decoder(nn.Module):
    def __init__(self, tokenizer):
        super(Decoder, self).__init__()
        self.tokenizer = tokenizer
        # 嵌入
        self.embed = nn.Embedding(
            num_embeddings=self.tokenizer.output_dict_len,
            embedding_dim=self.tokenizer.output_embed_dim,
            padding_idx=self.tokenizer.output_word2idx.get("<PAD>"),
        )
        # 抽取特征
        self.gru = nn.GRU(
            input_size=self.tokenizer.output_embed_dim,
            hidden_size=self.tokenizer.output_hidden_size,
            batch_first=False,
        )
        # 转换维度,做概率输出
        self.fc = nn.Linear(
            in_features=self.tokenizer.output_hidden_size,
            out_features=self.tokenizer.output_dict_len,
        )
    
    def forward_step(self, decoder_input, decoder_hidden):
        """
        单步解码:
            decoder_input: [1, batch_size]
            decoder_hidden: [1, batch_size, hidden_size]
        """
        # [1, batch_size] --> [1, batch_size, embedding_dim]
        decoder_input = self.embed(decoder_input)
        # 输入:[1, batch_size, embedding_dim] [1, batch_size, hidden_size]
        # 输出:[1, batch_size, hidden_size] [1, batch_size, hidden_size]
        # 因为只有1步,所以 out 跟 decoder_hidden是一样的
        out, decoder_hidden = self.gru(decoder_input, decoder_hidden)
        # [batch_size, hidden_size]
        out = out.squeeze(dim=0)
        # [batch_size, dict_len]
        out = self.fc(out)
        # out: [batch_size, dict_len]
        # decoder_hidden: [1, batch_size, hidden_size]
        return out, decoder_hidden

    def forward(self, encoder_hidden, y, y_len):
        """
        训练时的正向传播
            - encoder_hidden: [1, batch_size, hidden_size]
            - y: [seq_len, batch_size]
            - y_len: [batch_size]
        """
        # 计算输出的最大长度(本批数据的最大长度)
        output_max_len = max(y_len.tolist()) + 1
        # 本批数据的批量大小
        batch_size = encoder_hidden.size(1)
        # 输入信号 SOS  读取第0步,启动信号
        # decoder_input: [1, batch_size]
        # 输入信号 SOS [1, batch_size]
        decoder_input = torch.LongTensor(
            [[self.tokenizer.output_word2idx.get("<SOS>")] * batch_size]
        ).to(device=device)
        # 收集所有的预测结果
        # decoder_outputs: [seq_len, batch_size, dict_len]
        decoder_outputs = torch.zeros(
            output_max_len, batch_size, self.tokenizer.output_dict_len
        )
        # 隐藏状态 [1, batch_size, hidden_size]
        decoder_hidden = encoder_hidden
        # 手动循环
        for t in range(output_max_len):
            # 输入:decoder_input: [batch_size, dict_len], decoder_hidden: [1, batch_size, hidden_size]
            # 返回值:decoder_output_t: [batch_size, dict_len], decoder_hidden: [1, batch_size, hidden_size]
            decoder_output_t, decoder_hidden = self.forward_step(
                decoder_input, decoder_hidden
            )
            # 填充结果张量 [seq_len, batch_size, dict_len]
            decoder_outputs[t, :, :] = decoder_output_t
            # teacher forcing 教师强迫机制
            use_teacher_forcing = random.random() > 0.5
            # 0.5 概率 实行教师强迫
            if use_teacher_forcing:
                # [1, batch_size] 取标签中的下一个词
                decoder_input = y[t, :].unsqueeze(0)
            else:
                # 取出上一步的推理结果 [1, batch_size]
                decoder_input = decoder_output_t.argmax(dim=-1).unsqueeze(0)
        # decoder_outputs: [seq_len, batch_size, dict_len]
        return decoder_outputs

    # ...(其他函数暂略)

代码解析:

  • decoder定义了三个层:embed(词嵌入)、gru和fc(全链接层)。
  • 全链接层用于输出的是字典长度,即每个位置代表着每个字的概率。
  • decoder的forward_step方法,用于一步一步地执行,属于手动循环;forward方法,把所有步都执行完进行推理,属于自动循环。
  • forward方法中:
    • 首先,计算本批数据的最大长度(用于标签对齐)
    • 其次,使用encoder_hidden.size(1)获取批量大小
    • 然后,增加启动信号,即<SOS>
    • 然后,准备全0的张量 decoder_outputs
    • 然后,开始循环
      • 在循环每一步中,将输入和隐藏状态传给forward_step进行处理,得到输出概率decoder_output_t
      • 将结果概率放在decoder_outputs
      • 启用教师强迫机制(teacher forcing):
        • 即有50%概率,使用标准答案作为下一步的输入;
        • 否则,使用上一步的推理结果中概率最大的词作为下一步的输入。
    • 最后,返回结果概率张量 decoder_outputs

训练过程

  • 上述流程中较为重要的代码主要是 调用collate_fn具体训练过程手动循环进行正向推理
调用collate_fn
def collate_fn(batch, tokenizer):
    # 根据 x 的长度来 倒序排列
    batch = sorted(batch, key=lambda ele: ele[1], reverse=True)
    # 合并整个批量的每一部分
    input_sentences, input_sentence_lens, output_sentences, output_sentence_lens = zip(
        *batch
    )

    # 转索引【按本批量最大长度来填充】
    input_sentence_len = input_sentence_lens[0]
    input_idxes = []
    for input_sentence in input_sentences:
        input_idxes.append(tokenizer.encode_input(input_sentence, input_sentence_len))

    # 转索引【按本批量最大长度来填充】
    output_sentence_len = max(output_sentence_lens)
    output_idxes = []
    for output_sentence in output_sentences:
        output_idxes.append(
            tokenizer.encode_output(output_sentence, output_sentence_len)
        )

    # 转张量 [seq_len, batch_size]
    input_idxes = torch.LongTensor(input_idxes).t()
    output_idxes = torch.LongTensor(output_idxes).t()
    input_sentence_lens = torch.LongTensor(input_sentence_lens)
    output_sentence_lens = torch.LongTensor(output_sentence_lens)

    return input_idxes, input_sentence_lens, output_idxes, output_sentence_lens

代码解析:

  • 当文字长度不一样齐的时候,需要进行补充<PAD>,以保持所有序列长度一致

例如:
I’m a student.
I’m OK.
Here is your change.

  • 但是补充<PAD>本身对训练过程会造成干扰,所以我们需要采用一种机制:既保证对齐数据批量化训练,又能消除填充对训练过程的影响。
  • 这种机制原理:在训练时知道实际的数据长度,这样在训练时就可以略过<PAD>。
  • torch提供了相应的API,其大致过程是:
    • 首先,根据 x(上句) 的长度倒序排序
    • 其次,获取本批量最大的长度
    • 然后,将数据填充到本批量最大长度
    • 最后,在返回数据时,不知返回数据,还会带着真实长度
具体训练过程
    # (其他部分代码略)
        # 训练过程
        is_complete = False
        for epoch in range(self.epochs):
            self.model.train()
            for batch_idx, (x, x_len, y, y_len) in enumerate(train_dataloader):
                x = x.to(device=self.device)
                y = y.to(device=self.device)
                results = self.model(x, x_len, y, y_len)
                loss = self.get_loss(decoder_outputs=results, y=y)

                # 简单判定一下,如果损失小于0.5,则训练提前完成
                if loss.item() < 0.3:
                    is_complete = True
                    print(f"训练提前完成, 本批次损失为:{loss.item()}")
                    break

                loss.backward()
                self.optimizer.step()
                self.optimizer.zero_grad()
                # 过程监控
                with torch.no_grad():
                    if batch_idx % 100 == 0:
                        print(
                            f"第 {epoch + 1}{batch_idx + 1} 批, 当前批次损失: {loss.item()}"
                        )
                        x_true = self.get_real_input(x)
                        y_pred = self.model.batch_infer(x, x_len)
                        y_true = self.get_real_output(y)
                        samples = random.sample(population=range(x.size(1)), k=2)
                        for idx in samples:
                            print("\t真实输入:", x_true[idx])
                            print("\t真实结果:", y_true[idx])
                            print("\t预测结果:", y_pred[idx])
                            print(
                                "\t----------------------------------------------------------"
                            )

            # 外层提前退出
            if is_complete:
                # print("训练提前完成")
                break
        # 保存模型
        torch.save(obj=self.model.state_dict(), f="./model.pt")
手动循环进行正向推理
    #(其他部分略)
    def batch_infer(self, encoder_hidden):
        """
        推理时的正向传播
            - encoder_hidden: [1, batch_size, hidden_size]
        """
        # 推理时,设定一个最大的固定长度
        output_max_len = self.tokenizer.output_max_len
        # 获取批量大小
        batch_size = encoder_hidden.size(1)
        # 输入信号 SOS [1, batch_size]
        decoder_input = torch.LongTensor(
            [[self.tokenizer.output_word2idx.get("<SOS>")] * batch_size]
        ).to(device=device)
        # print(decoder_input)
        results = []
        # 隐藏状态
        # encoder_hidden: [1, batch_size, hidden_size]
        decoder_hidden = encoder_hidden
        with torch.no_grad():
            # 手动循环
            for t in range(output_max_len):
                # decoder_input: [1, batch_size]
                # decoder_hidden: [1, batch_size, hidden_size]
                decoder_output_t, decoder_hidden = self.forward_step(
                    decoder_input, decoder_hidden
                )
                # 取出结果 [1, batch_size]
                decoder_input = decoder_output_t.argmax(dim=-1).unsqueeze(0)
                results.append(decoder_input)
            # [seq_len, batch_size]
            results = torch.cat(tensors=results, dim=0)
        return results

代码解析:

  • 相比训练的时候,推理的时候函数入参没有y标准答案。
  • 推理的过程:
    • (与训练类似)获取最大长度、获取批量大小、构建启动信号。
    • (与训练不同)在无梯度环境里,调用forward_step函数,进行循环推理。
    • (与训练不同)因为推理时不需要teacher forcing机制,所以直接使用贪心思想获得概率最大的词。
    • 循环结束后,将结果拼接起来,返回。

补充知识

tqdm

定义

tqdm 是一个用于在 Python 中显示进度条的库,非常适合在长时间运行的循环中使用。

安装方法
pip install tqdm
使用方法
from tqdm import tqdm
import time

# 示例:在一个简单的循环中使用 tqdm
for i in tqdm(range(10)):
    time.sleep(1)  # 模拟某个耗时操作

运行结果:

OpenCC

定义

OpenCC(Open Chinese Convert)是一个用于简体中文和繁体中文之间转换的工具

安装方法
pip install OpenCC
使用方法
import opencc

# 创建转换器,使用简体到繁体的配置
converter = opencc.OpenCC('s2t')  # s2t: 简体到繁体

# 输入简体中文
simplified_text = "我爱编程"

# 进行转换
traditional_text = converter.convert(simplified_text)

print(traditional_text)  
# 输出结果:我愛編程

内容小结

  • Seq2Seq项目整体组成由tokenizer(分词器)、dataloader(数据加载)、encoder(编码器)、decoder(解码器)、seq2seq和main六个部分组成
  • 在分词器中重点工作是构建自定义字典,并添加特殊符号(special tokens)
    • <UNK>:表示未知单词,用于表示输入序列中未在字典中找到的单词;
    • <PAD>:表示填充符号,用于填充输入序列和输出序列,使它们具有相同的长度;
    • <SOS>:表示序列的开始,用于表示输出序列的起始位置;上文不会增加。
    • <EOS>:表示序列的结束,用于表示输出序列的结束位置,上文不会增加。
  • 在decoder的forward函数中,增加了一个teacher_forcing_ratio参数,用于控制是否使用教师强迫机制。
    • 有50%概率,使用标准答案作为下一步的输入;
    • 有50%概率,使用上一步的推理结果中概率最大的词作为下一步的输入。
    • 该机制用于提升训练速度。
  • 在训练过程中会使用collate_fn用于数据对齐时消除PAD的影响。

参考资料

(暂无)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1975719.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

想做linux内核开发,该怎么开始(上)

作为一名应届生在选择从事 Linux 内核开发这一职业领域时&#xff0c;需要系统地规划自己的职业道路&#xff0c;这将有助于你更准确地了解未来的发展方向并制定相应的学习和职业发展计划。在这篇文章中&#xff0c;我将向你介绍应届生在 Linux 内核开发领域的职业道路规划&…

O’Reilly

--江上往来人&#xff0c;但爱鲈鱼美。 --君看一叶舟&#xff0c;出没风波里。 OReilly OReilly出版社出版的技术类图书 俗称动物系列 应该是每个技术人员的必备手册。 OReilly动物系列&#xff08;中译本&#xff09; 简介" 动物系列作为 OReilly 书籍的典型代表被普遍…

【Apache Doris】周FAQ集锦:第 18 期

【Apache Doris】周FAQ集锦&#xff1a;第 18 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

基于级联深度学习算法在双参数MRI中检测前列腺病变的评估| 文献速递-AI辅助的放射影像疾病诊断

Title 题目 Evaluation of a Cascaded Deep Learning–based Algorithm for Prostate Lesion Detection at Biparametric MRI 基于级联深度学习算法在双参数MRI中检测前列腺病变的评估 Background 背景 Multiparametric MRI (mpMRI) improves prostate cancer (PCa) dete…

如何对我们要多次使用的页面进行一个抽取

有的时候,一个页面我们要多次使用,该怎么抽取呢? 创建一个文件夹,用于存放多次使用的页面 将要多次使用的组件(<template>)和风格(<style>)剪切出来,放入新建的页面 直接进行引用 导入 然后就可以使用

【FPGA设计】Vitis AI概述

一. Vitis AI简介 Vitis AI 是由 Xilinx&#xff08;现已被 AMD 收购&#xff09;提供的一套工具链和软件开发平台&#xff0c;用于简化和加速在基于 Xilinx FPGA 或自适应计算加速平台 (ACAP) 上部署深度学习推理应用的过程。Vitis AI 的目标是让开发者能够更容易地利用 FPGA…

python-素数回文数的个数(赛氪OJ)

[题目描述] 求 11 到 n 之间&#xff08;包括 n&#xff09;&#xff0c;既是素数又是回文数的整数有多少个。输入&#xff1a; 一个大于 11 小于 10000 的整数 n。输出&#xff1a; 11 到 n 之间的素数回文数个数。样例输入1 23 样例输出1 1 提示&#xff1a; 回文数指左右对…

【Python 逆向滑块】(实战五)逆向滑块,并实现用Python+Node.js 生成滑块、识别滑块、验证滑块、发送短信

逆向日期&#xff1a;2024.08.03 使用工具&#xff1a;Python&#xff0c;Node.js 本章知识&#xff1a;滑块距离识别&#xff0c;滑块轨迹生成&#xff0c;验证滑块并获取【validate】参数 文章难度&#xff1a;中等&#xff08;没耐心的请离开&#xff09; 文章全程已做去敏处…

MySQL:初识数据库初识SQL建库

目录 1、初识数据库 1.1 什么是数据库 1.2 什么是MySQL 2、数据库 2.1 数据库服务&数据库 2.2 C/S架构 3、 初识SQL 3.1 什么是SQL 3.2 SQL分类 4、使用SQL 4.1 查看所有数据库 4.1.2 语句解析 4.2 创建数据库 4.2.1 if not exists校验 4.2.2 手动明确字符集…

新款奔驰S450升级动态按摩座椅有哪些功能

奔驰 S450 升级前排动态按摩座椅通常具有以下功能&#xff1a; 1. 多种按摩模式和强度选择&#xff1a;通过精心设计的气囊和机械装置&#xff0c;能够模拟如揉捏、敲击、推拿等不同的按摩手法&#xff0c;为驾驶者和前排乘客舒缓肌肉疲劳&#xff0c;放松身心。 2. 广泛的按…

本地部署文生图模型 Flux

本地部署文生图模型 Flux 0. 引言1. 本地部署1-1. 创建虚拟环境1-2. 安装依赖模块1-3. 创建 Web UI1-4. 启动 Web UI1-5. 访问 Web UI 0. 引言 2024年8月1日&#xff0c;blackforestlabs.ai发布了 FLUX.1 模型套件。 FLUX.1 文本到图像模型套件&#xff0c;该套件定义了文本到…

2024年最有效的谷歌外链技巧!

在2024年&#xff0c;谷歌外链的战略在谷歌SEO领域依然占据重要地位。有效的外链战略不仅仅依赖于数量&#xff0c;更注重质量和结构的多样性。以下是一些最有效的策略 1.多样化的链接结构&#xff1a; 排名靠前的网站通常拥有复杂多元的外链结构。这意味着他们的链接来自不同…

【Python机器学习】支持向量机——SMO高效优化算法

最小化的目标函数、优化过程中必须要遵循的额约束条件。不久之前&#xff0c;人们使用二次规划求解工具来解决上述最优化问题&#xff0c;这种工具是一种用于在线性约束下优化具有多个变量的二次目标函数的软件&#xff0c;而这些二次规划求解工具需要强大的计算能力支撑&#…

一文搞懂后端面试之数据库MySQL的各种锁以及锁优化【中间件 | 数据库 | MySQL | 锁机制】

锁与索引 在MySQL的InnoDB引擎里&#xff0c;锁是借助索引来实现的&#xff0c;加锁锁住的其实是索引项&#xff0c;更加具体的说&#xff0c;是锁住了叶子节点。 引出的问题&#xff1a; 一个表有很多索引&#xff0c;锁的是哪个索引呢&#xff1f; 答案是 查询最终使用的索…

AI2-CUDA、CuDNN、TensorRT的详细安装教程

一、查看本机的显卡 首先你要看你的电脑是否有NVIDIA的独立显卡&#xff0c;你可以在设备管理器-显示适配器中查看 点击“开始”--找到“NVIDA Control Panel” 点击帮助--系统信息--组件&#xff0c;查看NVCUDA.DLL对应的产品名称&#xff0c;就可以看住CUDA的版本号 这里的版…

P31结构体初阶 (1)

结构体的声明 结构体的基础知识 结构是一些值的集合&#xff0c;这些值成为成员变量。结构的每个成员可以是不同类型的变量。 结构体的声明 结构成员的类型 结构的成员可以是标量、数组、指针&#xff0c;甚至是其他结构体 结构体变量的定义和初始化 结构体成员的访问 结构…

AVL树图解(插入与删除)

文章目录 AVL树概念平衡因子 旋转左单旋更新父节点与孩子节点的连接 右单旋左右双旋 (先左单旋再右单旋)右左双旋 (先右单旋再左单旋)验证是否为AVL树ALV树的删除操作一. 高度不变删除叶子节点和单孩子节点1.1高度不变删除叶子节点1.2删除单孩子节点 二. 高度变化 - 旋转2.1 左…

基于JAVA的企业财务管理系统设计与实现

点击下载源码 基于JAVA的企业财务管理系统设计与实现 摘要 对于企业集来说,财务管理的地位很重要。随着计算机和网络在企业中的广泛应用&#xff0c;企业发展速度在不断加快&#xff0c;在这种市场竞争冲击下企业财务管理系统必须优先发展&#xff0c;这样才能保证在竞争中处…

第30届哈尔滨种博会提质升级,10月28-30日移师长春全新亮相!

紧扣农业新质生产力发展需要&#xff0c;持续推动区域种业发展和农业振兴&#xff0c;第30届哈尔滨种业博览会暨第19届哈尔滨农资博览会/北方现代农业设施设备展将于10月28-30日在长春东北亚国际博览中心举办。展会扎根东北&#xff0c;全面辐射北方地区&#xff0c;被东北地区…