[PyTorch][chapter 47][LSTM -2]

news2024/11/25 3:44:53

目录:

  1.    双向LSTM
  2.    torch.nn.embedding()实现词嵌入层
  3.    nn.LSTM
  4.    nn.LSTMCell 
  5.    LSTM 情感分类例子

一  双向LSTM

      1 原理

      

     

      正向输出的结果是 h_t^1

      反向输出的结果是h_t^2

       nn.LSTM模块他在最后会将正向和反向的结果进行拼接concat.得到h_t

         o_t= f(h_t)

         \hat{y_t}=softmax(o_t)

# -*- coding: utf-8 -*-
"""
Created on Fri Aug  4 11:27:19 2023

@author: chengxf2
"""
import torch
import torch.nn  as nn


class MyLSTM(nn.Module):
 
    def __init__(self, input_size, hidden_size, nOut):
        
        super(MyLSTM, self).__init__()
 
        self.rnn = nn.LSTM(input_size, hidden_size, bidirectional=True)
        
        self.linear = nn.Linear(hidden_size * 2, nOut)
 
    def forward(self, input):
        #这里面的hidden 是concat 以后的结果
        hidden, _ = self.rnn(input)
        print("\n hidden ",hidden.shape) #[seq_len, batch_size, hidden_size*2]
        T, b, h = hidden.size()
        print(T,b,h)
        
        h_rec = hidden.view(T * b, h)
 
        output = self.linear(h_rec)  # [T * b, nOut]
        output = output.view(T, b, -1)
        print("\n out ",output.shape)
        return output
    


seq_len = 5
batch_size =1
input_size = 2
hidden_size = 10
N = 2
model = MyLSTM(input_size,hidden_size, N)
X = torch.randn((seq_len, batch_size, input_size))

output = model(X)


二 torch.nn.embedding()实现词嵌入层

意义
         
     输入: 词的编号索引,输出: 对应符号的嵌入向量。

参数:

参数

意思

Num_embeddings

词典的大小尺寸,比如总共出现100个词,那就输入100

embeddding _ dim

词对应向量的维度

padding_idx

输入长度为100,但是每次的句子长度并不一样,后面就需要用统一的数字填充,而这里就是指定这个数字,这样,网络在遇到填充id时,就不会计算其与其它符号的相关性。(初始化为0

max_norm

最大范数,如果嵌入向量的范数超过了这个界限,就要进行再归一化

norm_type

指定利用什么范数计算,并用于对比max_norm,默认为2范数

scale_grad_by_freq

根据单词在mini-batch中出现的频率,对梯度进行放缩。默认为False.

sparse

若为True,则与权重矩阵相关的梯度转变为稀疏张量。

# -*- coding: utf-8 -*-
"""
Created on Fri Aug  4 15:08:09 2023

@author: chengxf2
"""

import torch
import torch.nn as nn

word_to_idx = {'my':0,'name':1,'is':2,"jack":3}

num_embeddings = len(word_to_idx.keys())
embedding_dim = 10
#<class 'torch.nn.modules.sparse.Embedding'>
embeds = nn.Embedding(num_embeddings, embedding_dim)


text = 'is name'
text_idx = torch.LongTensor([word_to_idx[i] for i in text.split()])
#词嵌入得到词向量 [2,10]
hello_embed = embeds(text_idx)
print(hello_embed.shape, hello_embed.type)

   


三  nn.LSTM

1.1 模型参数

nn.LSTM  参数

作用

Input_size

 输入层的维度

Hidden _ size

 隐藏层的维数

Num_layers

堆叠的层数,默认值是1层,如果设置为2。第一层的隐藏值h,作为第二层的输入层的输入

bias

隐层状态是否带bias,默认为true

batch_first

默认False [T, batch_size, input_size]

dropout

默认值0

bidirectional

是否是双向 RNN,默认为:false

1.2 forward 定义 

     out,(h^T,c^T)=lstm(x,[h_{o},c_o])

     x shape:    [seq,batch_size, input_size]

     h,c shape: [num_layer, batch_size, hidden_size]

     out shape: [seq, batch_size, hidden_size]

# -*- coding: utf-8 -*-
"""
Created on Thu Aug  3 16:29:49 2023

@author: chengxf2
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim


lstm = nn.LSTM(input_size=50, hidden_size=20, num_layers=1)
print(lstm)

x = torch.randn(10,5,50)

out, (h,c)= lstm(x)

print("\n out shape ",out.shape)

print("\n hidden shape ",h.shape)

print("\n c shape ",c.shape)


四  nn.LSTMCell 

  2.1  参数基本一样,主要区别是forward 过程不一样

nn.LSTM  参数

作用

Input_size

 输入层的维度

Hidden _ size

 隐藏层的维数

Num_layers

堆叠的层数,默认值是1层,如果设置为2。第一层的隐藏值h,作为第二层的输入层的输入

2.2 forward

   h_t,c_t= lstm(x_t,[h_0,c_0]) 

   x_t  的shape:        [batch_size, input_size]

    h_t,c_t 的shape: [batch_size, input_size]

# -*- coding: utf-8 -*-
"""
Created on Thu Aug  3 16:39:42 2023

@author: chengxf2
"""
import  torch
import  torch.nn as nn
import  torch.nn.functional as F

print('lstmCell')



batch_size =2
input_size = 20
hidden_size =10
seq_num = 5
cell = nn.LSTMCell(input_size, hidden_size)
X = torch.randn((seq_num,batch_size,input_size))
H0 = torch.zeros(batch_size,hidden_size)
C0=  torch.zeros(batch_size, hidden_size)

for xt in X:
    
    ht,ct = cell(xt,[H0,C0])
    
print("\n ht.shape",ht.shape)
print("\n ct.shape",ht.shape)


五  LSTM 情感分类

      5.1 环境安装

        torch text 有兼容性要求

pip install  torchtext==0.11.0  --user
pip install  SpaCy
安装完可以打印看一下,版本是否兼容
# -*- coding: utf-8 -*-
"""
Created on Mon Aug  7 15:49:15 2023

@author: chengxf2
"""

import torch
import torchtext

print(torch.__version__)
print(torchtext.__version__)

-----------------------------
runfile('D:/AI/LSTM/untitled0.py', wdir='D:/AI/LSTM')
1.10.0+cpu
0.11.0

1.2  加载数据集

  文件名: loadcorpus.py

import torch
from torchtext.legacy import data
from torchtext.legacy import datasets


def load_data():
    '''
    Step 1: Create a dataset object
    
    legacy code:
    Field class is used for data processing, including tokenizer and numberzation. 
    To check out the dataset, users need to first set up the TEXT/LABEL fields.
    '''
    
    
    TEXT = data.Field(tokenize=data.get_tokenizer('basic_english'),
                  init_token='', eos_token='', lower=True)
    LABEL = data.LabelField(dtype = torch.long)
    
    
    
    # 按照(TEXT, LABEL) 分割成 训练集:25000,测试集:25000
    legacy_train, legacy_test = datasets.IMDB.splits(TEXT, LABEL)  # datasets here refers to torchtext.legacy.datasets

    
    print('len of train data:', len(legacy_train))        # 25000
    print('len of test data:', len(legacy_test))          # 25000
     
    # torchtext.data.Example : 用来表示一个样本,数据+标签
    #print(legacy_test.examples[15].text)                 #文本:句子的单词列表:字符串
    #print(legacy_train.examples[15].label)                # 标签: 字符串
    
    
    
    
    return TEXT, LABEL,legacy_train, legacy_test
    

def  create_vocabulary(TEXT,LABEL, legacy_train):
    
    '''
    Step 2 Build the data processing pipeline
    
    legacy code:

     The default tokenizer implemented in the Field class is the built-in python split() function.
     Users choose the tokenizer by calling data.get_tokenizer(), 
     and add it to the Field constructor.
     
     For the sequence model:
     it's common to append <BOS> (begin-of-sentence)
     and <EOS> (end-of-sentence) tokens, 
     and the special tokens need to be defined in the Field class.
     
     Things you can do with a vocabuary object

        1: Total length of the vocabulary
        2: String2Index (stoi) and Index2String (itos)
        3:  A purpose-specific vocabulary which contains word appearing more than N times
    '''
  
    TEXT.build_vocab(legacy_train,max_size=9997)
    LABEL.build_vocab(legacy_train)
    

    legacy_vocab = TEXT.vocab


    #10003
    vocab_size = len(legacy_vocab)
    print("\n  length of the TEXT vocab is", vocab_size)
    print("\n length of the LABEL vocab is", len(LABEL.vocab))
  
    #print('pretrained_embedding:', pretrained_embedding.shape)    # torch.Size([10002, 100])

    
    legacy_stoi = legacy_vocab.stoi
    #print("The index of 'example' is", legacy_stoi['example'])

    
    legacy_itos = legacy_vocab.itos
    #print("The token at index 466 is: ", legacy_itos[466])

    # Set up the mim_freq value in the Vocab class
    #TEXT.build_vocab(legacy_train, min_freq=10)
    #legacy_vocab2 = TEXT.vocab
    #print("The length of the legacy vocab is: ", len(legacy_vocab2))
    
    


 
 
    return vocab_size
    


def  create_iterator(batchs ,train_data, test_data):
    
    '''
    Step 3: Generate batch iterator

    legacy code:
     To train a model efficiently,
     it's recommended to build an iterator to generate data batch.
    '''
    
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    
    legacy_train_iterator, legacy_test_iterator = data.Iterator.splits(
    (train_data, test_data), batch_size=batchs, device = device)
    
    
    return legacy_train_iterator, legacy_test_iterator 
    

def  iterator_data(legacy_train_iterator):
    
        '''
        Step 4: Iterate batch to train a model
        
        batch.text.shape:  [seq_len, batch_size]
        '''
    
        for i, batch in enumerate(legacy_train_iterator):

 
            continue
            #print("\n shape: ",batch.text.shape,"\t i:",i,"\t text: ",batch.text[:,0][0:3])


     
    
def load_corpus():
    
    print("\n ==> Step 1: Create a dataset object ")
    TEXT, LABEL,train_data, test_data = load_data()
    
    print("\n ==> Step 2: Build the data processing pipeline")
    vocab_size= create_vocabulary(TEXT, LABEL, train_data)
    
    print("\n ==> Step 3: Generate batch iterator")
    legacy_train_iterator, legacy_test_iterator  =create_iterator(30, train_data, test_data)
    
    #print("\n ==> Step 4: iterator_data ")
    #iterator_data(legacy_train_iterator)
    
    
  
    
    return vocab_size,legacy_train_iterator, legacy_test_iterator 

1.3 创建模型

    文件名: lstmModule

# -*- coding: utf-8 -*-
"""
Created on Mon Aug  7 11:58:41 2023

@author: chengxf2
"""

import torch
import torch.nn as nn

class LSTM(nn.Module):
    
    def __init__(self, vocab_size,embedding_dim, hidden_dim,bidirectional):
        
        super(LSTM, self).__init__()
        
        self.category_num = 1 #最后分类的种类,二分类为1
        self.bidirectional = 2#双向
        #[0-10001]=>[100]
        #vovcab_size: 单词数量  embedding_dim: 词向量维度
        self.embedding =nn.Embedding(vocab_size, embedding_dim)
        
        #[100]=>[256]
        #双向LSTM,FC层使用hidden_dim*2
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=2,
                            bidirectional=bidirectional, dropout= 0.5)
        #[256*2]=>1
        self.fc = nn.Linear(hidden_dim*2 , self.category_num)
        self.dropout = nn.Dropout(0.5)
        
        if True == bidirectional:
             self.bidirectional = 2
        
        
    
    
    def forward(self, X):
        
        '''
        X: [seq_len, batch] 开始输入的是词的索引构成的向量
        '''
    

        #转换为向量形式[seq_len, batch]=>[seq_len, batch, input_size]
        embedding = self.embedding(X)
        embedding = self.dropout(embedding)
        
        #output.shape: [seq, batch_size,hidd_dim*2] 实际上就是隐藏层
        #hidden.shape: [num_layer*self.bidirectional,  batch_size, hid_dim]
        #cell.shape:   [num_layer*self.bidirectional,  batch_size, hid_dim]
        output, (hidden, cell) = self.lstm(embedding)
        #print("\n output",output.shape, "\t hidden ",hidden.shape, "\t cell ",cell.shape)
 
        #双向,要把最后两个输出拼接  hidden.shape :torch.Size([4, 30, 100])
        if 2 == self.bidirectional:
            output = torch.cat([hidden[-2], hidden[-1]], dim=1)
        
     
        #output.shape [batch_size, hid_dim*2]
        output = self.dropout(output)
        
        #[seq_num, category_num]
        out = self.fc(output)
        return out
    
    

1.4 main.py 训练部分

# -*- coding: utf-8 -*-
"""
Created on Tue Aug  8 10:06:05 2023

@author: chengxf2
"""


import torch
from torch import nn
from torch.nn import functional as F
import lstmModule
from   lstmModule import LSTM as lstm
import loadcorpus
from loadcorpus import load_corpus
from torch import optim
import numpy as np

'''
def predict():
    #模型预测
    for batch in test_iterator:
        # batch_size个预测
        preds = rnn(batch.text).squeeze(1)
        preds = predice_test(preds)
        # print(preds)
 
        i = 0
        for text in batch.text:
            # 遍历一句话里的每个单词
            for word in text:
                print(TEXT.vocab.itos[word], end=' ')
        
            print('')
            # 输出3句话
            if i == 3:
                break
            i = i + 1
 
        i = 0
        for pred in preds:
            idx = int(pred.item())
            print(idx, LABEL.vocab.itos[idx])
            # 输出3个结果(标签)
            if i == 3:
                break
            i = i + 1
        break

'''
def evaluate(rnn, iterator, criteon):
    '''
    数据集分为3部分:
    train, validate, test
    训练的时候:
         每轮结束要用validate 数据集来验证一下,防止过拟合
    '''
    avg_acc = []
    rnn.eval()         # 表示进入测试模式
 
    with torch.no_grad():
        for batch in iterator:
            pred = rnn(batch.text).squeeze(1)      # [b, 1] => [b]
       
            acc = binary_acc(pred, batch.label).item()
            avg_acc.append(acc)
 
    avg_acc = np.array(avg_acc).mean()
 
    print('test acc:', avg_acc)

def binary_acc(preds, y):
    '''定义一个函数用于计算准确率
    '''
    preds = torch.round(torch.sigmoid(preds))
    correct = torch.eq(preds, y).float()
    acc = correct.sum() / len(y)
    return acc


def train(model, iterator, optimizer, criteon):
    #训练函数
    avg_acc = []
    model.train()   # 表示进入训练模式
    
    for i, batch in enumerate(iterator):
        # [seq, b] => [b, 1] => [b]
        # batch.text 就是上面forward函数的参数text,压缩维度是为了和batch.label维度一致
        pred = model(batch.text)
        #pred.shape: [seq,1]=>[seq]
        pred = pred.squeeze(1)
        target = batch.label.float()
        loss = criteon(pred, target)
        # 计算每个batch的准确率
        acc = binary_acc(pred, batch.label).item()
        avg_acc.append(acc)
 
        optimizer.zero_grad()  # 清零梯度准备计算
        loss.backward()        # 反向传播
        optimizer.step()       # 更新训练参数
 
        if i % 2 == 0:
            print("\n i:%d"%i,"\t acc : %4.2f"%acc)
 
    avg_acc = np.array(avg_acc).mean()
    print('avg acc:', avg_acc)
    

def main():
    
    print("---main---")
    maxIter = 5
    input_size = 128
    hidden_size = 256
    vocab_size,train_iterator, test_iterator = load_corpus()
    
    net = lstm(vocab_size, input_size, hidden_size,True)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    optimizer = optim.Adam(net.parameters(), lr=1e-4)
    
    
    # BCEWithLogitsLoss是针对二分类的CrossEntropy
    criteon = nn.BCEWithLogitsLoss()
    
    criteon.to(device)
    net.to(device)
    
  
 

 
    
    print("\n ---train--")
    for epoch in range(maxIter):
        
       # 训练模型
        train(net, train_iterator, optimizer, criteon)
        # 评估模型
        evaluate(net, test_iterator, criteon)

if __name__ == "__main__":
    
     main()

参考:

深度学习与Pytorch入门实战(十六)情感分类实战(基于IMDB数据集)_Douzi1024的博客-CSDN博客

https://github.com/pytorch/text/blob/master/examples/legacy_tutorial/migration_tutorial.ipynb

LSTM情感分类(上) - 知乎

Google Colab 快速上手 - 知乎

深度学习与Pytorch入门实战(十六)情感分类实战(基于IMDB数据集)_Douzi1024的博客-CSDN博客

https://github.com/pytorch/text/blob/master/examples/legacy_tutorial/migration_tutorial.ipynb

https://github.com/renjunxiang/Text-Classification/blob/master/TextClassification/data/data_single.csv

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

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

相关文章

Java 生产初学常用注解

目录 0. 基础语法逻辑运算符继承抛出异常获取数据方式泛型 1. 接收前端数据&#xff08;controller&#xff09;QueryWrapper2. service 层注解 3. Dao 层&#xff08;与数据库交互&#xff09;3.1 mybatis-plus中BaseMapper 4. ELK框架es配置sql参数logstash数据读取csv数据读…

使用go-zero快速构建微服务

本文是对 使用go-zero快速构建微服务[1]的亲手实践 编写API Gateway代码 mkdir bookstore && cd bookstorego mod init bookstore mkdir api && goctl api -o api/bookstore.api syntax "v1"info(title: "xx使用go-zero"desc: "xx用…

springboot(6)

Fastclass机制&#xff1a; 为一个对象创建对应的Fastclass对象&#xff0c;对象的各个方法会创建索引index关联到fastclass对象&#xff0c;每个index对应一个方法&#xff0c;之后只需要通过对象实例以及index&#xff0c;调用invoke(instance,index,args)&#xff0c;即可调…

今天面了个00后测试员,让我见识到什么才是内卷届的天花板...

深耕IT行业多年&#xff0c;我们发现&#xff0c;对于一个程序员而言&#xff0c;能去到一线互联网公司&#xff0c;会给我们以后的发展带来多大的影响。 很多人想说&#xff0c;这个我也知道&#xff0c;但是进大厂实在是太难了&#xff0c;简历投出去基本石沉大海&#xff0…

【软件工程】3 ATM系统的设计

目录 3 ATM系统的设计 3.1体系结构设计 3.2 设计模式选择 3.3 补充、完善类图 3.4 数据库设计 3.4.1 类与表的映射关系 3.4.2 数据库设计规范 3.4.3 数据库表 3.5 界面设计 3.5.1 界面结构设计 3.5.2 界面设计 3.5.2.1 功能界面设计 3.5.2.2 交互界面 总博客&…

【性能类】—浏览器渲染机制

一、什么是DOCTYPE及作用 DTD&#xff08;文档类型定义&#xff09;&#xff1a;是一系列的语法规则&#xff0c;用来定义XML或HTML的文档类型。浏览器会使用它来判断文档类型&#xff0c;决定使用何种协议来解析&#xff0c;以及切换浏览器模式 解释&#xff1a;DTD就定义DOC…

【13】SAP ABAP性能优化 - 共享对象 (Shared Objects)

1. 背景 “共享对象”是NetWeaver 6.40以上版本ABAP编程中的一个技术&#xff0c;在"共享对象"概念出来之前&#xff0c;在ABAP中可以通过EXPORT和IMPORT这样的关键字去访问服务器上的共享内存&#xff0c;实现不同进程中的数据交互。有关这方面的概念&#xff0c;我…

5分钟,带你了解低代码开发

在传统的理解中&#xff0c;企业内数字化应用的开发和迭代应该是 IT 部门的工作&#xff0c;但事实并非如此。一方面&#xff0c;激烈的市场竞争和反复出现的疫情给数字化提出了新的要求&#xff1b;另一方面&#xff0c;五花八门的零代码、低代码工具正如雨后春笋一般出现&…

【设计模式】——模板模式

什么是模板模式&#xff1f; 模板方法模式&#xff08;Template Method Pattern&#xff09;&#xff0c;又叫模板模式(Template Pattern)&#xff0c;在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现&#xff0c;但调用将以抽象类中定义的方式进行…

代码审计-RCE命令执行漏洞审计

代码审计必备知识点&#xff1a; 1、代码审计开始前准备&#xff1a; 环境搭建使用&#xff0c;工具插件安装使用&#xff0c;掌握各种漏洞原理及利用,代码开发类知识点。 2、代码审计前信息收集&#xff1a; 审计目标的程序名&#xff0c;版本&#xff0c;当前环境(系统,中间件…

通达信上涨回调选股公式,趋势指标和摆动指标结合使用

在前面的文章中&#xff0c;介绍了赫尔均线 (HMA)和随机RSI(StochRSI)&#xff0c;这两个指标分别属于趋势指标和摆动指标。趋势指标和摆动指标是技术分析中常用的两类指标&#xff0c;用于分析市场的走势和波动&#xff0c;它们的计算方法、应用场景都是有区别的。今天利用两类…

架构实践方法

一、识别复杂度 将主要的复杂度问题列出来&#xff0c;然后根据业务、技术、团队等综合情况进行排序&#xff0c;优先解决当前面临的最主要的复杂度问题。对于按照复杂度优先级解决的方式&#xff0c;存在一个普遍的担忧&#xff1a;如果按照优先级来解决复杂度&#xff0c;可…

从安装 Seata 开始的分布式事务之旅 springboot集成seata

从安装 Seata 开始的分布式事务之旅 介绍什么是 Seata&#xff1f; 安装 Seata Server下载 Seata Server 发行版配置Seata解压文件配置Seata的yml文件把配置文件config.txt加载到nacos上修改config.txt文件加载到nacos上 启动Seata服务正常启动查看启动日志打开控制台页面 启动…

使用 PowerShell 将 Excel 中的每个工作表单独另存为独立的文件

导语&#xff1a;在日常工作中&#xff0c;我们经常需要处理 Excel 文件。本文介绍了如何使用 PowerShell 脚本将一个 Excel 文件中的每个工作表单独另存为独立的 Excel 文件&#xff0c;以提高工作效率。 1. 准备工作 在开始之前&#xff0c;请确保已经安装了 Microsoft Exc…

IMV7.0

一、背景 经历了多个版本&#xff0c;基础内容在前面&#xff0c;可以使用之前的基础环境&#xff1a; v1&#xff1a; https://blog.csdn.net/wtt234/article/details/132139454 v2&#xff1a; https://blog.csdn.net/wtt234/article/details/132144907 v3&#xff1a; https…

Vue调用硬件 接口报错

谷歌浏览器调用硬件报错 报错原因 调用身份证读卡器&#xff0c;使用谷歌浏览器 读卡器硬件的接口 有几率是被谷歌拦截 所以报错 在谷歌地址栏输入 chrome://flags/ 搜索 block 找到这个选项 切换状态之后重启浏览器即可 当时找的这篇文章 解决问题 参考链接 如果大家的问题没…

2023集成电路产业发展与产教融合高峰论坛会议顺利举行

8月5日&#xff0c;由中国半导体行业协会和市政府共同主办&#xff0c;天水师范学院、天水华天科技股份有限公司、杭州加速科技有限公司承办的2023集成电路产业发展与产教融合高峰论坛在天水举行。天水市委书记冯文戈&#xff0c;教育部学生服务与素质发展中心副主任方伟&#…

untiy 连接两个UI或一段固定一段跟随鼠标移动的线段

注意&#xff0c;仅适用于UI&#xff0c;且Canvas必须是Camera模式&#xff0c;不能用在3D物体上&#xff0c;3D物体请使用LineRenender 先创建一个图片&#xff0c;将锚点固定在左边 然后在脚本中添加如下内容 public RectTransform startObj;//起点物体public RectTransfor…

软件测试基本准侧与方法摘录

软件测试基本准侧与方法的摘录&#xff08;应用实例待补充&#xff09; 写在最开始&#xff1a;“测试是为发现错误而执行程序的过程”。————《软件测试的艺术》 &#x1f929; 本文中很多概念描述摘抄自还有很多概念没有列举。已写的部分概念缺少相应的实例&#xff0c;尚…

Java课题笔记~ Spring事务的程序举例环境搭建

举例&#xff1a;购买商品 trans_sale 项目 本例要实现购买商品&#xff0c;模拟用户下订单&#xff0c;向订单表添加销售记录&#xff0c;从商品表减少库存。 实现步骤&#xff1a; Step0&#xff1a;创建数据库表 创建两个数据库表 sale , goods sale 销售表&#xff1a;…