w03_nlp大模型训练·处理字符串

模型1

实现一个自行构造的找规律(机器学习)任务:
输入一个字符串,根据字符a所在位置进行分类,对比rnn和pooling做法

python 复制代码
# coding:utf8
import random
import json
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

"""
模型1
基于pytorch框架编写模型训练
实现一个自行构造的找规律(机器学习)任务
输入一个字符串,根据字符a所在位置进行分类
对比rnn和pooling做法
"""

class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim)  #embedding层
        # self.pool = nn.AvgPool1d(sentence_length)   #池化层
        #可以自行尝试切换使用rnn
        self.rnn = nn.RNN(vector_dim, vector_dim, batch_first=True)
        # +1的原因是可能出现a不存在的情况,那时的真实label在构造数据时设为了sentence_length
        self.classify = nn.Linear(vector_dim, sentence_length + 1)
        self.loss = nn.functional.cross_entropy

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)
        #使用pooling的情况
        # x = x.transpose(1, 2)
        # x = self.pool(x)
        # x = x.squeeze()
        #使用rnn的情况
        rnn_out, hidden = self.rnn(x)
        # # x = rnn_out[:, -1, :]  #或者写hidden.squeeze()也是可以的,因为rnn的hidden就是最后一个位置的输出
        x = hidden.squeeze()
        #接线性层做分类
        y_pred = self.classify(x)
        if y is not None:
            return self.loss(y_pred, y)   #预测值和真实值计算损失
        else:
            return y_pred                 #输出预测结果

#字符集随便挑了一些字,实际上还可以扩充
#为每个字生成一个标号
#{"a":1, "b":2, "c":3...}
#abc -> [1,2,3]
def build_vocab():
    chars = "abcdefghijk"  #字符集
    vocab = {"pad":0}
    for index, char in enumerate(chars):
        vocab[char] = index+1   #每个字对应一个序号
    vocab['unk'] = len(vocab) #26
    return vocab

#随机生成一个样本
def build_sample(vocab, sentence_length):
    #注意这里用sample,是不放回的采样,每个字母不会重复出现,但是要求字符串长度要小于词表长度
    x = random.sample(list(vocab.keys()), sentence_length)
    #指定哪些字出现时为正样本
    if "a" in x:
        y = x.index("a")
    else:
        y = sentence_length
    x = [vocab.get(word, vocab['unk']) for word in x]   #将字转换成序号,为了做embedding
    return x, y

#建立数据集
#输入需要的样本数量。需要多少生成多少
def build_dataset(sample_size, vocab, sentence_length):
    dataset_x = []
    dataset_y = []
    for i in range(sample_size):
        x, y = build_sample(vocab, sentence_length)
        dataset_x.append(x)
        dataset_y.append(y)
    return torch.LongTensor(dataset_x), torch.LongTensor(dataset_y)

#建立模型
def build_model(vocab, char_dim, sentence_length):
    model = TorchModel(char_dim, sentence_length, vocab)
    return model

#测试代码
#用来测试每轮模型的准确率
def evaluate(model, vocab, sentence_length):
    model.eval()
    x, y = build_dataset(200, vocab, sentence_length)   #建立200个用于测试的样本
    print("本次预测集中共有%d个样本"%(len(y)))
    correct, wrong = 0, 0
    with torch.no_grad():
        y_pred = model(x)      #模型预测
        for y_p, y_t in zip(y_pred, y):  #与真实标签进行对比
            if int(torch.argmax(y_p)) == int(y_t):
                correct += 1
            else:
                wrong += 1
    print("正确预测个数:%d, 正确率:%f"%(correct, correct/(correct+wrong)))
    return correct/(correct+wrong)


def main():
    #配置参数
    epoch_num = 20        #训练轮数
    batch_size = 40       #每次训练样本个数
    train_sample = 1000    #每轮训练总共训练的样本总数
    char_dim = 30         #每个字的维度
    sentence_length = 10   #样本文本长度
    learning_rate = 0.001 #学习率
    # 建立字表
    vocab = build_vocab()
    # 建立模型
    model = build_model(vocab, char_dim, sentence_length)
    # 选择优化器
    optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
    log = []
    # 训练过程
    for epoch in range(epoch_num):
        model.train()
        watch_loss = []
        for batch in range(int(train_sample / batch_size)):
            x, y = build_dataset(batch_size, vocab, sentence_length) #构造一组训练样本
            optim.zero_grad()    #梯度归零
            loss = model(x, y)   #计算loss
            loss.backward()      #计算梯度
            optim.step()         #更新权重
            watch_loss.append(loss.item())
        print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
        acc = evaluate(model, vocab, sentence_length)   #测试本轮模型结果
        log.append([acc, np.mean(watch_loss)])
    #画图
    plt.plot(range(len(log)), [l[0] for l in log], label="acc")  #画acc曲线
    plt.plot(range(len(log)), [l[1] for l in log], label="loss")  #画loss曲线
    plt.legend()
    plt.show()
    #保存模型
    torch.save(model.state_dict(), "model.pth")
    # 保存词表
    writer = open("vocab.json", "w", encoding="utf8")
    writer.write(json.dumps(vocab, ensure_ascii=False, indent=2))
    writer.close()
    return

#使用训练好的模型做预测
def predict(model_path, vocab_path, input_strings):
    char_dim = 30  # 每个字的维度
    sentence_length = 10  # 样本文本长度
    vocab = json.load(open(vocab_path, "r", encoding="utf8")) #加载字符表
    model = build_model(vocab, char_dim, sentence_length)     #建立模型
    model.load_state_dict(torch.load(model_path))             #加载训练好的权重
    x = []
    for input_string in input_strings:
        x.append([vocab[char] for char in input_string])  #将输入序列化
    model.eval()   #测试模式
    with torch.no_grad():  #不计算梯度
        result = model.forward(torch.LongTensor(x))  #模型预测
    for i, input_string in enumerate(input_strings):
        print("输入:%s, 预测类别:%s, 概率值:%s" % (input_string, torch.argmax(result[i]), result[i])) #打印结果



if __name__ == "__main__":
    main()
    test_strings = ["kijabcdefh", "gijkbcdeaf", "gkijadfbec", "kijhdefacb"]
    predict("model.pth", "vocab.json", test_strings)

代码分析:

1. 导入必要的库
python 复制代码
import random
import json
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
  • random:用于生成随机数据,如在构建样本时进行随机采样。
  • json:用于处理 JSON 数据,用于保存和加载词汇表。
  • torchtorch.nn:PyTorch 深度学习框架及其神经网络模块,用于构建和训练模型。
  • numpy:用于辅助数据处理和计算。
  • matplotlib.pyplot:用于绘制训练过程中的准确率和损失曲线。
2. 定义神经网络模型类 TorchModel
python 复制代码
class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0)
        self.rnn = nn.RNN(vector_dim, vector_dim, batch_first=True)
        self.classify = nn.Linear(vector_dim, sentence_length+1)
        self.loss = nn.functional.cross_entropy

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)           # batch_size * sentence_length -> batch_size * sentence_length * vector_dim
        rnn_out, hidden = self.rnn(x)   # batch_size * sentence_length * vector_dim -> batch_size * 1 * vector_dim
        x = hidden.squeeze()            # batch_size * 1 * vector_dim -> batch_size * vector_dim
        y_p = self.classify(x)          # batch_size * vector_dim -> batch_size * (sentence_length+1)
        if y is not None:
            return self.loss(y_p, y)
        else:
            return y_p
  • __init__ 方法

    • self.embedding = nn.Embedding(len(vocab), vector_dim):创建一个嵌入层,将输入的字符索引映射到 vector_dim 维的向量空间,len(vocab) 表示词汇表的大小,该层将离散的字符转换为连续的向量表示,有助于模型学习语义信息。
    • self.rnn = nn.RNN(vector_dim, vector_dim, batch_first=True):定义一个循环神经网络(RNN)层,输入和输出维度都是 vector_dim,batch_first=True 表示输入数据的第一个维度是批次大小,这样输入形状为 (batch_size, sequence_length, vector_dim)。
    • self.classify = nn.Linear(vector_dim, sentence_length + 1):创建一个线性层,将 RNN 的输出映射到 sentence_length + 1 个类别,因为要对字符 a 的位置进行分类,加 1 是为了处理字符 a 不存在的情况(此时标签为 sentence_length
    • self.loss = nn.functional.cross_entropy:使用交叉熵损失函数,适用于多分类任务,用于计算预测结果和真实标签之间的损失。
  • forward 方法

    • x = self.embedding(x):将输入的字符索引序列通过嵌入层转换为嵌入向量,输入 x 的形状为 (batch_size, sequence_length),输出形状为 (batch_size, sequence_length, vector_dim)
    • rnn_out, hidden = self.rnn(x):将嵌入向量输入 RNN 层,得到 RNN 的输出 rnn_out最终隐藏状态 hidden,其中 hidden 是最后一个时间步的隐藏状态。
    • x = hidden.squeeze()将最终隐藏状态 hidden 的维度进行压缩,因为 hidden 的形状可能包含额外的维度(如 (batch_size, 1, vector_dim)),压缩后形状为 (batch_size, vector_dim)。
    • y_pred = self.classify(x)将压缩后的隐藏状态通过线性层进行分类,输出形状为 (batch_size, sentence_length + 1),表示每个样本属于各个位置的预测概率分布。
    • 如果提供了真实标签 y,则计算并返回预测结果和真实标签之间的交叉熵损失,否则只返回预测结果。
3. 数据相关函数
python 复制代码
#为每个字生成一个标号
#{"a":1, "b":2, "c":3...}
#abc -> [1,2,3]
def build_vocab():
    chars = "abcdefghijklmnopqrstuvwxyz0123456789"  # 字符集
    vocab = {"pad":0}
    for index, char in enumerate(chars):
        vocab[char] = index+1   # 每个字对应一个序号
    vocab['unk'] = len(vocab)  # 36
    return vocab

#随机生成一个样本
def build_sample(vocab, sentence_length):
    #注意这里用sample,是不放回的采样,每个字母不会重复出现,但是要求字符串长度要小于词表长度
    x = random.sample(list(vocab.keys()), sentence_length)
    #指定哪些字出现时为正样本
    if "a" in x:
        y = x.index("a")
    else:
        y = sentence_length
    x = [vocab.get(word, vocab['unk']) for word in x]   #将字转换成序号,为了做embedding
    return x, y

#建立数据集
#输入需要的样本数量。需要多少生成多少
def build_dataset(sample_length, vocab, sentence_length):
    dataset_x = []
    dataset_y = []
    for i in range(sample_length):
        x, y = build_sample(vocab, sentence_length)
        dataset_x.append(x)
        dataset_y.append(y)
    return torch.LongTensor(dataset_x), torch.LongTensor(dataset_y)
  • build_vocab 函数

    • 构建词汇表,为每个字符分配一个唯一的整数索引,包括字母和数字,将 "pad" 设为 0,"unk" 设为词汇表的长度。
  • build_sample 函数

    • 从词汇表中随机抽取 sentence_length 个字符(不放回抽样),组成输入字符串 x
    • 如果字符 "a"x 中,将其位置作为标签 y,否则将 y 设为 sentence_length
    • 将字符转换为其在词汇表中的索引,便于后续的嵌入层处理。
  • build_dataset 函数

    • 生成 sample_length 个样本,通过调用 build_sample 函数,并将生成的样本的输入 x 和标签 y 分别存储在 dataset_xdataset_y 中。
    • 将存储样本的列表转换为 torch.LongTensor 类型,返回生成的数据集。
4. 建立模型函数 build_model
python 复制代码
def build_model(char_dim, sentence_length, vocab):
    model = TorchModel(char_dim, sentence_length, vocab)
    return model
  • 该函数根据输入的字符维度 char_dim、句子长度 sentence_length 和词汇表 vocab 创建 TorchModel 实例并返回。
5. 评估函数 evaluate
python 复制代码
#测试代码
#用来测试每轮模型的准确率
def evaluate(model, vocab, sentence_length):
    model.eval()
    x, y = build_dataset(200, vocab, sentence_length)   #建立200个用于测试的样本
    print("本次预测集中共有%d个样本"%(len(y)))
    correct, wrong = 0, 0
    with torch.no_grad():
        y_pred = model(x)      #模型预测
        for y_p, y_t in zip(y_pred, y):  #与真实标签进行对比
            if int(torch.argmax(y_p)) == int(y_t):
                correct += 1
            else:
                wrong += 1
    print("正确预测个数:%d, 正确率:%f"%(correct, correct/(correct+wrong)))
    return correct/(correct+wrong)
  • 将模型设置为评估模式,避免更新参数。
  • 生成包含 200 个样本的测试集,使用模型进行预测得到 y_pred
  • 对于每个样本,通过 torch.argmax(y_p) 找到预测概率最大的类别索引,将其与真实标签 y_t 比较,统计正确和错误的预测数量。
  • 计算并输出准确率。
6. 主函数 main
python 复制代码
def main():
    #配置参数
    epoch_num = 35      #训练轮数
    batch_size = 40       #每次训练样本个数
    train_sample = 1000    #每轮训练总共训练的样本总数
    char_dim = 30         #每个字的维度
    sentence_length = 10   #样本文本长度
    learning_rate = 0.001 #学习率
    # 建立字表
    vocab = build_vocab()
    # 建立模型
    model = build_model(char_dim, sentence_length, vocab)
    # 选择优化器
    optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
    log = []
    # 训练过程
    for epoch in range(epoch_num):
        model.train()
        watch_loss = []
        for batch in range(int(train_sample / batch_size)):
            x, y = build_dataset(batch_size, vocab, sentence_length) #构造一组训练样本
            loss = model(x, y)   #计算loss
            loss.backward()      #计算梯度
            optim.step()         #更新权重
            optim.zero_grad()    #梯度归零
            watch_loss.append(loss.item())
        print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
        acc = evaluate(model, vocab, sentence_length)   #测试本轮模型结果
        log.append([acc, np.mean(watch_loss)])
    #画图
    plt.plot(range(len(log)), [l[0] for l in log], label="acc")  #画acc曲线
    plt.plot(range(len(log)), [l[1] for l in log], label="loss")  #画loss曲线
    plt.legend()
    plt.show()
    #保存模型
    torch.save(model.state_dict(), "model.path")
    # 保存词表
    writer = open("vocab.json", "w", encoding="utf8")
    writer.write(json.dumps(vocab, ensure_ascii=False, indent=2))
    writer.close()
    return
  • 参数设置:设置训练轮数、批次大小、样本总数、字符维度、句子长度和学习率等参数。
  • 建立词汇表 :调用 build_vocab 生成词汇表。
  • 创建模型和优化器 :使用 build_model 创建模型,使用 Adam 优化器进行训练。
  • 训练过程
    • 循环 epoch_num 轮,每轮创建多个批次的训练样本,计算损失,进行反向传播和参数更新,记录每批的损失并计算平均损失。
    • 每轮结束后调用 evaluate 函数评估模型,记录准确率和平均损失到 log 中。
  • 可视化和保存 :使用 matplotlib 绘制准确率和损失曲线,保存模型的状态字典和词汇表。
7. 预测函数 predict
python 复制代码
def predict(model_path, vocab_path, input_strings):
    char_dim = 30
    sentence_length = 10
    vocab = json.load(open(vocab_path, "r", encoding="utf8"))
    model = build_model(char_dim, sentence_length, vocab)
    model.load_state_dict(torch.load(model_path, weights_only=True))
    x = []
    for input_string in input_strings:
        x.append([vocab[char] for char in input_string])
    model.eval()
    with torch.no_grad():
        result = model.forward(torch.LongTensor(x))
    for i, input_string in enumerate(input_strings):
        print("输入:%s, 预测类别:%s, 概率值:%s" % (input_string, torch.argmax(result[i]), result[i]))
  • 从文件加载词汇表和模型权重。
  • 将输入字符串中的字符转换为词汇表中的索引。
  • 将模型设置为评估模式,不计算梯度,使用模型进行预测。
  • 对于每个输入字符串,打印预测的类别(通过 torch.argmax(result[i]) 得到概率最大的类别)和对应的概率分布。
8. 主程序入口
python 复制代码
if __name__ == "__main__":
    main()
    test_strings = ["kijabcdefh", "gijkbcdeaf", "gkijadfbec", "kijhdefacb"]
    predict("model.path", "vocab.json", test_strings)
  • 调用 main 函数进行模型的训练和保存操作。
  • 定义测试字符串,调用 predict 函数对测试字符串进行预测并输出结果。

模型2

实现一个网络完成一个简单nlp任务:判断文本中是否有某些特定字符出现

python 复制代码
#coding:utf8

import torch
import torch.nn as nn
import numpy as np
import random
import json
import matplotlib.pyplot as plt

"""
模型2
基于pytorch的网络编写
实现一个网络完成一个简单nlp任务
判断文本中是否有某些特定字符出现
"""

class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0)  #embedding层
        self.pool = nn.AvgPool1d(sentence_length)   #池化层
        self.classify = nn.Linear(vector_dim, 1)     #线性层
        self.activation = torch.sigmoid     #sigmoid归一化函数
        self.loss = nn.functional.mse_loss  #loss函数采用均方差损失

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)                      #(batch_size, sen_len) -> (batch_size, sen_len, vector_dim)
        x = x.transpose(1, 2)                      #(batch_size, sen_len, vector_dim) -> (batch_size, vector_dim, sen_len)
        x = self.pool(x)                           #(batch_size, vector_dim, sen_len)->(batch_size, vector_dim, 1)
        x = x.squeeze()                            #(batch_size, vector_dim, 1) -> (batch_size, vector_dim)
        x = self.classify(x)                       #(batch_size, vector_dim) -> (batch_size, 1) 3*5 5*1 -> 3*1
        y_pred = self.activation(x)                #(batch_size, 1) -> (batch_size, 1)
        if y is not None:
            return self.loss(y_pred, y)   #预测值和真实值计算损失
        else:
            return y_pred                 #输出预测结果

#字符集随便挑了一些字,实际上还可以扩充
#为每个字生成一个标号
#{"a":1, "b":2, "c":3...}
#abc -> [1,2,3]
def build_vocab():
    chars = "你我他defghijklmnopqrstuvwxyz"  #字符集
    vocab = {"pad":0}
    for index, char in enumerate(chars):
        vocab[char] = index+1   #每个字对应一个序号
    vocab['unk'] = len(vocab) #26
    return vocab

#随机生成一个样本
#从所有字中选取sentence_length个字
#反之为负样本
def build_sample(vocab, sentence_length):
    #随机从字表选取sentence_length个字,可能重复
    x = [random.choice(list(vocab.keys())) for _ in range(sentence_length)]
    #指定哪些字出现时为正样本
    if set("你我他") & set(x):
        y = 1
    #指定字都未出现,则为负样本
    else:
        y = 0
    x = [vocab.get(word, vocab['unk']) for word in x]   #将字转换成序号,为了做embedding
    return x, y

#建立数据集
#输入需要的样本数量。需要多少生成多少
def build_dataset(sample_length, vocab, sentence_length):
    dataset_x = []
    dataset_y = []
    for i in range(sample_length):
        x, y = build_sample(vocab, sentence_length)
        dataset_x.append(x)
        dataset_y.append([y])
    return torch.LongTensor(dataset_x), torch.FloatTensor(dataset_y)

#建立模型
def build_model(vocab, char_dim, sentence_length):
    model = TorchModel(char_dim, sentence_length, vocab)
    return model

#测试代码
#用来测试每轮模型的准确率
def evaluate(model, vocab, sample_length):
    model.eval()
    x, y = build_dataset(200, vocab, sample_length)   #建立200个用于测试的样本
    print("本次预测集中共有%d个正样本,%d个负样本"%(sum(y), 200 - sum(y)))
    correct, wrong = 0, 0
    with torch.no_grad():
        y_pred = model(x)      #模型预测
        for y_p, y_t in zip(y_pred, y):  #与真实标签进行对比
            if float(y_p) < 0.5 and int(y_t) == 0:
                correct += 1   #负样本判断正确
            elif float(y_p) >= 0.5 and int(y_t) == 1:
                correct += 1   #正样本判断正确
            else:
                wrong += 1
    print("正确预测个数:%d, 正确率:%f"%(correct, correct/(correct+wrong)))
    return correct/(correct+wrong)


def main():
    #配置参数
    epoch_num = 10        #训练轮数
    batch_size = 20       #每次训练样本个数
    train_sample = 500    #每轮训练总共训练的样本总数
    char_dim = 20         #每个字的维度
    sentence_length = 6   #样本文本长度
    learning_rate = 0.005 #学习率
    # 建立字表
    vocab = build_vocab()
    # 建立模型
    model = build_model(vocab, char_dim, sentence_length)
    # 选择优化器
    optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
    log = []
    # 训练过程
    for epoch in range(epoch_num):
        model.train()
        watch_loss = []
        for batch in range(int(train_sample / batch_size)):
            x, y = build_dataset(batch_size, vocab, sentence_length) #构造一组训练样本
            optim.zero_grad()    #梯度归零
            loss = model(x, y)   #计算loss
            loss.backward()      #计算梯度
            optim.step()         #更新权重
            watch_loss.append(loss.item())
        print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
        acc = evaluate(model, vocab, sentence_length)   #测试本轮模型结果
        log.append([acc, np.mean(watch_loss)])
    #画图
    plt.plot(range(len(log)), [l[0] for l in log], label="acc")  #画acc曲线
    plt.plot(range(len(log)), [l[1] for l in log], label="loss")  #画loss曲线
    plt.legend()
    plt.show()
    #保存模型
    torch.save(model.state_dict(), "model.pth")
    # 保存词表
    writer = open("vocab.json", "w", encoding="utf8")
    writer.write(json.dumps(vocab, ensure_ascii=False, indent=2))
    writer.close()
    return

#使用训练好的模型做预测
def predict(model_path, vocab_path, input_strings):
    char_dim = 20  # 每个字的维度
    sentence_length = 6  # 样本文本长度
    vocab = json.load(open(vocab_path, "r", encoding="utf8")) #加载字符表
    model = build_model(vocab, char_dim, sentence_length)     #建立模型
    model.load_state_dict(torch.load(model_path))             #加载训练好的权重
    x = []
    for input_string in input_strings:
        x.append([vocab[char] for char in input_string])  #将输入序列化
    model.eval()   #测试模式
    with torch.no_grad():  #不计算梯度
        result = model.forward(torch.LongTensor(x))  #模型预测
    for i, input_string in enumerate(input_strings):
        print("输入:%s, 预测类别:%d, 概率值:%f" % (input_string, round(float(result[i])), result[i])) #打印结果



if __name__ == "__main__":
    main()
    test_strings = ["fnvfee", "wz你dfg", "rqwdeg", "n我kwww"]
    predict("model.pth", "vocab.json", test_strings)

代码分析:

1. 导入必要的库
python 复制代码
import torch
import torch.nn as nn
import numpy as np
import random
import json
import matplotlib.pyplot as plt

导入了多个常用的 Python 库,其中 torchtorch.nn 用于构建和训练神经网络;numpy 用于数值计算;random 用于生成随机数据;json 用于处理字符表的保存和读取;matplotlib.pyplot 用于可视化训练过程中的准确率和损失曲线(虽然在 main 函数中相关绘图代码被注释掉了)。

2. 定义神经网络模型类 TorchModel
python 复制代码
class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0)  
        self.pool = nn.AvgPool1d(sentence_length)  
        self.classify = nn.Linear(vector_dim, 1)    
        self.activation = torch.sigmoid    
        self.loss = nn.functional.mse_loss  

    def forward(self, x, y=None):
        x = self.embedding(x)                      
        x = x.transpose(1, 2)                      
        x = self.pool(x)                           
        x = x.squeeze()                            
        x = self.classify(x)                       
        y_pred = self.activation(x)                
        if y is not None:
            return self.loss(y_pred, y)  
        else:
            return y_pred  
  • __init__ 方法

    • self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0):定义了一个嵌入层(Embedding Layer),它将输入的文本中每个字符对应的序号(基于 vocab 字典)转换为一个固定维度(vector_dim)的向量表示,用于捕捉字符的语义信息。padding_idx=0 表示当输入文本进行填充(比如使不同长度的文本长度一致时),序号为 0 的位置对应的嵌入向量会被特殊处理(通常是全零向量)。
    • self.pool = nn.AvgPool1d(sentence_length):创建了一个一维平均池化层,用于对经过嵌入层后的文本向量表示进行池化操作,通过平均的方式在文本长度维度上进行降维,提取文本的整体特征。
    • self.classify = nn.Linear(vector_dim, 1):定义了一个线性层,将经过池化后得到的固定维度(vector_dim)的特征向量映射到一个单一维度,用于输出最终的预测值(这里是判断文本是否包含特定字符的二分类结果)。
    • self.activation = torch.sigmoid:指定了**sigmoid 激活函数,用于将线性层的输出转换为概率值,使得输出结果在 (0, 1) 区间内,符合二分类任务中对概率的要求** (概率大于 0.5 可视为正类,小于 0.5 视为负类)。
    • self.loss = nn.functional.mse_loss选择均方差损失(Mean Squared Error Loss)作为损失函数,用于衡量模型预测值与真实标签之间的差异,虽然在二分类任务中更常用交叉熵损失,但这里选择了均方差损失来进行模型训练。
  • forward 方法

    • x = self.embedding(x)将输入的文本序号表示(形状通常为 (batch_size, sen_len),即批次大小和文本长度)转换为对应的嵌入向量表示,输出形状变为 (batch_size, sen_len, vector_dim)
    • x = x.transpose(1, 2):对嵌入向量的维度进行转置,将文本长度维度和向量维度交换位置,变为 (batch_size, vector_dim, sen_len),以便后续池化操作能在文本长度维度上正确进行。
    • x = self.pool(x):在文本长度维度上进行平均池化操作,将文本长度维度压缩为 1,输出形状变为 (batch_size, vector_dim, 1)
    • x = x.squeeze():移除维度为 1 的维度(这里是文本长度维度),得到形状为 (batch_size, vector_dim) 的特征向量。
    • x = self.classify(x)通过线性层将特征向量映射到单一维度,输出形状变为 (batch_size, 1)
    • y_pred = self.activation(x)对线性层的输出应用 sigmoid 激活函数,得到最终的预测概率值,形状仍为 (batch_size, 1)
    • 如果传入了真实标签 y,则返回预测值与真实标签之间的损失值;如果没有传入 y,则返回预测结果(预测概率值)。
3. 数据相关函数
  • build_vocab 函数
python 复制代码
def build_vocab():
    chars = "你我他defghijklmnopqrstuvwxyz"  
    vocab = {"pad":0}
    for index, char in enumerate(chars):
        vocab[char] = index+1  
    vocab['unk'] = len(vocab) 
    return vocab

用于构建字符表(词汇表),将给定的字符集合中的每个字符赋予一个唯一的序号,从 1 开始编号(序号 0 留给 pad,用于文本填充操作),同时为不在给定字符集合中的未知字符定义了一个统一的序号(即最后一个序号 len(vocab)),最终返回构建好的字符表字典。

  • build_sample 函数
python 复制代码
def build_sample(vocab, sentence_length):
    x = [random.choice(list(vocab.keys())) for _ in range(sentence_length)]
    if set("你我他") & set(x):
        y = 1
    else:
        y = 0
    x = [vocab.get(word, vocab['unk']) for word in x]  
    return x, y

随机生成一个训练或测试样本,先从字符表中随机选取指定长度(sentence_length)的字符组成文本 x,然后根据文本 x 中是否包含特定字符("你""我""他")来确定样本的标签 y(包含为正样本,标签为 1;不包含为负样本,标签为 0),最后将文本中的字符转换为对应的序号表示(通过字符表 vocab),返回文本序号表示 x 和标签 y

  • build_dataset 函数
python 复制代码
def build_dataset(sample_length, vocab, sentence_length):
    dataset_x = []
    dataset_y = []
    for i in range(sample_length):
        x, y = build_sample(vocab, sentence_length)
        dataset_x.append(x)
        dataset_y.append([y])
    return torch.LongTensor(dataset_x), torch.FloatTensor(dataset_y)

根据指定的样本数量(sample_length),通过循环调用 build_sample 函数来生成多个样本,将生成的文本序号表示收集到 dataset_x 列表中,对应的标签收集到 dataset_y 列表中,最后分别将这两个列表转换为 torch.LongTensor(用于存储文本序号,整数类型)和 torch.FloatTensor(用于存储标签,浮点数类型,虽然这里标签是 0 或 1 的二分类情况,但用浮点数类型方便后续计算损失等操作),并返回这两个张量作为数据集。

4. 模型相关函数
  • build_model 函数
python 复制代码
def build_model(char_dim, sentence_length, vocab):
    model = TorchModel(char_dim, sentence_length, vocab)
    return model

根据给定的字符维度(char_dim,即嵌入向量的维度)、文本长度(sentence_length)和字符表(vocab)创建 TorchModel 模型实例,并返回该模型,方便后续进行训练等操作。

5. 模型评估函数 evaluate
python 复制代码
def evaluate(model, vocab, sample_length):
    model.eval()
    x, y = build_dataset(200, vocab, sample_length)  
    print("本次预测集中共有%d个正样本,%d个负样本"%(sum(y), 200 - sum(y)))
    correct, wrong = 0, 0
    with torch.no_grad():
        y_pred = model(x)      
        for y_p, y_t in zip(y_pred, y):  
            if float(y_p) < 0.5 and int(y_t) == 0:
                correct += 1  
            elif float(y_p) >= 0.5 and int(y_t) == 1:
                correct += 1  
            else:
                wrong += 1
    print("正确预测个数:%d, 正确率:%f"%(correct, correct/(correct+wrong)))
    return correct/(correct+wrong)

用于评估给定模型在测试集上的准确率。首先将模型设置为评估模式(model.eval()),以避免在评估过程中进行梯度更新等训练相关操作。然后生成包含 200 个样本的测试集数据(通过调用 build_dataset 函数),并统计测试集中正样本和负样本的数量。接着在不计算梯度的情况下(with torch.no_grad()),使用模型对测试集进行预测得到预测结果 y_pred,再将预测结果与真实标签 y 逐个进行对比,根据预测概率值和真实标签判断预测是否正确,统计正确预测的个数,计算并返回准确率。

6. 主函数 main
python 复制代码
def main():
    # 配置参数
    epoch_num = 10        
    batch_size = 20       
    train_sample = 500    
    char_dim = 20         
    sentence_length = 6   
    learning_rate = 0.005 
    vocab = build_vocab()
    model = build_model(char_dim, sentence_length, vocab)
    optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
    log = []
    for epoch in range(epoch_num):
        model.train()
        watch_loss = []
        for batch in range(int(train_sample / batch_size)):
            x, y = build_dataset(batch_size, vocab, sentence_length) 
            loss = model(x, y)  
            loss.backward()      
            optim.step()         
            optim.zero_grad()    
            watch_loss.append(loss.item())
        print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
        acc = evaluate(model, vocab, sentence_length)  
        log.append([acc, np.mean(watch_loss)])
    torch.save(model.state_dict(), "model.pth")
    writer = open("vocab.json", "w", encoding="utf8")
    writer.write(json.dumps(vocab, ensure_ascii=False, indent=2))
    writer.close()
    return
  • main 函数中:
    • 首先配置了一系列训练相关的参数,包括训练轮数(epoch_num)、批次大小(batch_size)、每轮训练的样本总数(train_sample)、字符维度(char_dim)、文本长度(sentence_length)以及学习率(learning_rate)。
    • 接着通过 build_vocab 函数构建字符表,通过 build_model 函数创建模型实例,然后选择 Adam 优化器用于在训练过程中更新模型的参数。
    • 创建一个空列表 log,用于记录每轮训练后的准确率和平均损失值。
    • 进入训练循环,对于每一轮训练:
      • 首先将模型设置为训练模式(model.train()),以便进行梯度更新等训练操作。
      • 在每一轮中,通过循环构造多个批次的训练样本(通过调用 build_dataset 函数获取每个批次的输入 x 和标签 y),然后计算该批次数据在当前模型下的损失值(loss = model(x, y)),接着进行反向传播(loss.backward())和权重更新(optim.step())等操作,并将该批次的损失值添加到 watch_loss 列表中,最后将梯度归零(optim.zero_grad())。
      • 每轮训练结束后,计算该轮的平均损失值并打印出来,然后通过 evaluate 函数对当前轮次训练后的模型在测试集上进行准确率评估,将评估得到的准确率和平均损失值添加到 log 列表中。
    • 训练完成后,将训练好的模型权重保存到文件 "model.pth" 中,同时将字符表以 json 格式保存到文件 "vocab.json" 中,方便后续加载使用。
7. 模型预测函数 predict
python 复制代码
def predict(model_path, vocab_path, input_strings):
    char_dim = 20  
    sentence_length = 6  
    vocab = json.load(open(vocab_path, "r", encoding="utf8")) 
    model = build_model(char_dim, sentence_length, vocab)     
    model.load_state_dict(torch.load(model_path, weights_only=True))             
    x = []
    for input_string in input_strings:
        x.append([vocab[char] for char in input_string])  
    model.eval()  
    with torch.no_grad():  
        result = model.forward(torch.LongTensor(x))  
    for i, input_string in enumerate(input_strings):
        print("输入:%s, 预测类别:%d, 概率值:%f" % (input_string, round(float(result[i])), result[i]))

用于使用已经训练好的模型对输入的文本字符串进行预测。首先根据给定的字符维度和文本长度以及加载的字符表创建模型实例,然后加载已经保存的模型权重(通过 torch.load 函数,设置 weights_only=True 表示只加载权重数据)。接着将输入的文本字符串根据字符表转换为对应的序号表示,并将其转换为 torch.LongTensor 类型。将模型设置为评估模式(model.eval())且在不计算梯度的情况下(with torch.no_grad()),使用模型对输入的文本序号表示进行预测,得到预测结果,最后逐个打印出每个输入文本字符串对应的预测类别(根据预测概率值判断,四舍五入取整)和概率值。

8. 主程序入口
python 复制代码
if __name__ == "__main__":
    main()
    test_strings = ["fnvfee", "wz你dfg", "rqwdeg", "n我kwww"]
    predict("model.pth", "vocab.json", test_strings)

当脚本作为主程序运行时(if __name__ == "__main__"),首先执行 main 函数进行模型的训练、评估以及相关数据(模型权重和字符表)的保存操作。然后定义了一组测试文本字符串 test_strings,并通过 predict 函数使用训练好的模型对这些测试文本进行预测,输出每个测试文本的预测类别和概率值。

模型对比分析

1、两个模型定义的不同之处

模型架构

第一个模型(判断特定字符在字符串的第几个位置):

python 复制代码
class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0)
        self.rnn = nn.RNN(vector_dim, vector_dim, batch_first=True)
        self.classify = nn.Linear(vector_dim, sentence_length+1)
        self.loss = nn.functional.cross_entropy

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)           # batch_size * sentence_length -> batch_size * sentence_length * vector_dim
        rnn_out, hidden = self.rnn(x)   # batch_size * sentence_length * vector_dim -> batch_size * 1 * vector_dim
        x = hidden.squeeze()            # batch_size * 1 * vector_dim -> batch_size * vector_dim
        y_p = self.classify(x)          # batch_size * vector_dim -> batch_size * (sentence_length+1)
        if y is not None:
            return self.loss(y_p, y)
        else:
            return y_p
  • 架构特点
    • 使用了 nn.RNN 作为序列处理层,旨在处理输入序列中的顺序信息。
    • 最终通过线性层 self.classifier 将 RNN 的最终隐藏状态映射到 num_classes 个类别用于预测特定字符在字符串中的位置,这是一个多分类问题,类别数等于字符串长度加一(加一是为了处理字符不存在的情况)。
    • 采用 CrossEntropyLoss 作为损失函数,适合多分类任务,计算预测位置类别和真实位置类别之间的差异。

第二个模型(判断文本中是否有某些特定字符出现):

python 复制代码
class TorchModel(nn.Module):
    def __init__(self, vector_dim, sentence_length, vocab):
        super(TorchModel, self).__init__()
        self.embedding = nn.Embedding(len(vocab), vector_dim, padding_idx=0)  #embedding层
        self.pool = nn.AvgPool1d(sentence_length)   #池化层
        self.classify = nn.Linear(vector_dim, 1)     #线性层
        self.activation = torch.sigmoid     #sigmoid归一化函数
        self.loss = nn.functional.mse_loss  #loss函数采用均方差损失

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)                      #(batch_size, sen_len) -> (batch_size, sen_len, vector_dim)
        x = x.transpose(1, 2)                      #(batch_size, sen_len, vector_dim) -> (batch_size, vector_dim, sen_len)
        x = self.pool(x)                           #(batch_size, vector_dim, sen_len)->(batch_size, vector_dim, 1)
        x = x.squeeze()                            #(batch_size, vector_dim, 1) -> (batch_size, vector_dim)
        x = self.classify(x)                       #(batch_size, vector_dim) -> (batch_size, 1) 3*5 5*1 -> 3*1
        y_pred = self.activation(x)                #(batch_size, 1) -> (batch_size, 1)
        if y is not None:
            return self.loss(y_pred, y)   #预测值和真实值计算损失
        else:
            return y_pred                 #输出预测结果
  • 架构特点
    • 包含一个 nn.Embedding 层,将字符索引转换为向量表示。
    • 使用 nn.AvgPool1d 对嵌入后的序列进行平均池化,将 sentence_length 维度压缩,旨在对整个句子进行聚合信息。
    • 一个线性层 self.classify 将池化后的向量映射到一个单一维度,因为这里是一个二分类问题(特定字符出现或不出现)。
    • 使用 torch.sigmoid 激活函数将线性层输出映射到 (0, 1) 范围,代表概率。
    • 采用 nn.functional.mse_loss 作为损失函数,计算预测概率和真实标签(0 或 1)之间的均方误差。
不同之处及其原因
  • 网络结构

    • 第一个模型
      • 使用 RNN 层是因为任务需要考虑输入序列的顺序信息,以便找到特定字符在序列中的位置 。RNN 能够处理序列数据,通过对序列的逐步处理,将序列信息逐步编码到隐藏状态中,最终的隐藏状态可用于预测字符位置
      • 多分类的线性层是因为任务是预测字符在序列中的位置,可能的位置数量等于序列长度加一,需要输出每个位置的概率。
    • 第二个模型
      • 平均池化层 nn.AvgPool1d 是为了对整个句子的嵌入表示进行汇总,将句子中字符的信息聚合为一个固定长度的向量,因为这里并不关心字符的顺序,只关心是否包含某些特定字符,所以通过池化可以简化计算。
      • 单分类的线性层是因为任务是二分类问题,判断是否出现某些特定字符,输出一个值,然后通过 sigmoid 激活得到概率。
  • 损失函数

    • 第一个模型
      • 使用 CrossEntropyLoss 是因为该任务是多分类任务,它能有效地计算多分类预测结果与真实类别之间的差异,鼓励模型输出正确类别概率最大。
    • 第二个模型
      • 使用 nn.functional.mse_loss 是因为该任务是二分类任务,但使用了 sigmoid 激活函数将输出映射到概率,MSE 可以计算预测概率和真实标签(0 或 1)的均方误差,不过对于二分类任务,nn.BCELoss 可能是更合适的选择。
  • 数据处理和任务性质

    • 第一个模型
      • build_sample 函数中,数据生成和标签赋值是根据特定字符的位置,并且要确保样本具有足够的多样性和长度,以训练模型找到字符位置的模式。
    • 第二个模型
      • build_sample 函数中,数据生成和标签赋值是根据是否包含特定字符(如 "你我他"),将包含这些字符的样本标记为正样本,不包含的标记为负样本,更关注字符的存在性而不是位置。
其他方面的不同
  • 评估和预测逻辑

    • 第一个模型
      • evaluate 函数中,使用 torch.argmax 来找到预测的位置类别,并与真实位置类别比较计算准确率,适用于多分类任务。
    • 第二个模型
      • evaluate 函数中,将预测概率和真实标签(0 或 1)比较,根据 sigmoid 输出的概率是否大于 0.5 来判断是否为正样本,适用于二分类任务。
  • 训练过程中的细微差别

    • 第一个模型
      • 可能需要更多的训练数据和训练轮次,因为 RNN 学习序列信息相对复杂,且是多分类任务,训练过程中需要捕捉更复杂的模式
    • 第二个模型
      • 由于是二分类且使用了简单的池化和线性结构,可能相对更容易收敛,但可能需要注意 MSE 作为二分类损失函数可能导致的一些训练不稳定或收敛速度问题。

综上所述,两个模型的差异主要源于不同的任务需求,一个是多分类任务(预测字符位置),另一个是二分类任务(判断特定字符是否出现),因此在网络结构、损失函数、数据处理和评估逻辑上都进行了相应的调整以适配各自的任务特点。

在实际使用中,可以根据具体任务需求选择合适的模型结构和训练策略,对于第二个模型,可以考虑将 nn.functional.mse_loss 替换为 nn.BCELoss 以提高性能,对于第一个模型,可以考虑使用更复杂的 RNN 变种(如 LSTM 或 GRU)或添加更多的层来提高性能。

2、为什么模型1不能用池化层,模型2可以用池化?

  1. 模型 1 的任务特点与池化层的不适用性
    • 任务性质
      • 模型 1 的任务是判断特定字符在字符串中的位置,这是一个序列相关的多分类任务。例如,对于字符串 "abcde",字符 "c" 的位置是 2,模型需要学习字符在序列中的顺序信息来准确预测位置。
    • 池化层的问题
      • 池化层(如平均池化或最大池化)会丢失序列的顺序信息。以平均池化为例,它会对序列中的元素进行某种统计操作(如求平均值),将序列长度维度压缩。在这个过程中,字符之间的先后顺序关系被忽略了。例如,对于两个字符串 "abc" 和 "cba",经过池化操作后可能得到相同的结果,但是对于模型 1 来说,这两个字符串中特定字符的位置是不同的,这种顺序信息的丢失会导致模型无法准确完成位置预测任务。
  2. 模型 2 的任务特点与池化层的适用性
    • 任务性质
      • 模型 2 的任务是判断文本中是否有某些特定字符出现,这是一个二分类任务。例如,对于字符串 "abcde",只需要判断其中是否包含特定字符(如 "a"),而不需要关心字符的具体位置。
    • 池化层的优势
      • 池化层在这里可以有效地聚合句子级别的信息。在模型 2 中,经过嵌入层后,句子中的每个字符都有了对应的向量表示。池化层(如平均池化)可以将这些字符向量在句子长度维度上进行聚合,得到一个固定长度的向量,这个向量可以代表整个句子的整体特征。例如,对于一个句子 "我是中国人",池化层可以将每个字的嵌入向量聚合起来,得到一个能够表示这个句子是否包含特定字符(如 "我")的整体特征向量,从而方便后续的二分类判断。

3、为什么模型1不能使用全连接神经网络,而模型2可以?

  1. 模型 1 不能简单使用全连接神经网络的原因

    • 任务本质与序列信息的重要性
      • 模型 1 的任务是判断特定字符在字符串中的位置,这是一个高度依赖序列顺序信息的任务。例如,对于字符串 "abcde" 和 "edcba",虽然字符集合相同,但特定字符(如 "a")的位置不同。全连接神经网络(Fully - Connected Neural Network,FCNN)本身没有对序列顺序的内在感知机制。
    • FCNN 处理序列的局限性
      • **FCNN 在处理输入时,会将每个输入特征独立看待,忽略了输入数据之间的顺序关系。**对于模型 1,如果将字符串的字符索引表示直接输入 FCNN,它无法理解字符在序列中的先后顺序,也就难以准确判断特定字符的位置。例如,在一个长度为 10 的字符串中,FCNN 可能会学习到字符索引与位置的某种关联,但这种关联不依赖于字符在字符串中的真实顺序,导致无法正确完成任务。
    • 需要处理变长序列的挑战
      • 输入的字符串长度可能是变化的,而 FCNN 通常需要固定的输入维度。 如果要使用 FCNN 处理不同长度的字符串,就需要对输入进行复杂的预处理(如填充或截断),并且很难有效地利用序列中的顺序信息来处理这种变长的情况。而 RNN 等序列模型可以自然地处理变长序列,通过在每个时间步更新隐藏状态来整合序列信息。
  2. 模型 2 可以使用全连接神经网络的原因

    • 任务本质对顺序信息的低依赖
      • **模型 2 的任务是判断文本中是否有某些特定字符出现,这是一个相对简单的二分类任务,主要关注特定字符是否存在于文本中,对字符的顺序信息依赖程度较低。**例如,对于字符串 "abc" 和 "cba",只要判断是否包含目标字符(如 "a")即可,字符顺序并不影响任务结果。
    • 池化层与 FCNN 的有效结合
      • 模型 2 使用了池化层来聚合句子级别的信息。池化操作将字符嵌入向量在句子长度维度上进行了汇总,得到一个固定长度的向量,这个向量代表了句子的整体特征。然后,全连接神经网络可以很好地处理这个固定长度的向量,将其映射到二分类的输出 。例如,经过平均池化后,句子 "我爱学习" 的字符嵌入向量被聚合为一个向量,FCNN 可以基于这个向量判断是否包含目标字符(如 "爱"),有效地完成二分类任务。
    • 简单的任务模式与 FCNN 的适配性
      • **二分类任务的模式相对简单,FCNN 在处理这种从固定长度向量到二分类输出的映射时具有足够的能力。**它可以学习到池化后的向量特征与目标类别(是否包含特定字符)之间的关系,通过调整权重来实现有效的分类。

4、模型1的RNN内部进行了什么操作?

  1. RNN 的基本原理回顾
    • RNN(循环神经网络)是一种专门用于处理序列数据的神经网络。它的核心思想是在处理序列的每个元素时,都利用了之前元素处理后的信息,通过一个循环结构来实现这种信息的传递。
  2. 模型 1 中 RNN 的输入和初始化
    • 在模型 1 中,输入x首先经过嵌入层得到嵌入向量表示。假设输入x的形状是(batch_size, sequence_length)batch_size是批次大小,sequence_length是字符串长度),经过嵌入层后形状变为(batch_size, sequence_length, embedding_dim)。这个嵌入后的向量作为 RNN 层self.rnn的输入。
    • RNN 有一个初始隐藏状态h_0,其维度通常是(batch_size, hidden_size)hidden_size是 RNN 层内部定义的隐藏状态维度)。在 PyTorch 中,如果没有特别指定,这个初始隐藏状态会被自动初始化。
  3. RNN 内部的核心操作 - 循环计算
    • 对于序列中的每个时间步(每个字符对应的位置):
      • 输入和隐藏状态的结合 :在每个时间步t,RNN 将当前时间步的输入x_t(形状为(batch_size, embedding_dim),x_t是x在时间步t的切片)和上一个时间步的隐藏状态h_{t - 1}(形状为(batch_size, hidden_size))结合起来。这种结合的方式是通过矩阵乘法和加法操作实现的,具体的计算方式取决于 RNN 的公式定义。
      • 激活函数应用:结合后的结果通过一个激活函数(如果是基本的 RNN,通常是**tanh函数**)来更新隐藏状态。例如,更新后的隐藏状态h_t的计算可能类似于h_t = tanh(W_{ih}x_t + W_{hh}h_{t - 1}+ b_h),其中W_{ih}是输入到隐藏层的权重矩阵,W_{hh}是隐藏层到隐藏层的权重矩阵,b_h是偏置项。这个更新后的隐藏状态h_t包含了从序列开始到当前时间步的所有信息。
    • 这个过程在每个时间步重复,直到处理完整个序列(即sequence_length个时间步)。
  4. RNN 的输出和在模型 1 中的使用
    • RNN 的输出包括每个时间步的隐藏状态序列和最后一个时间步的隐藏状态。 在模型 1 中,通过rnn_out, hidden = self.rnn(x)获取了这两个输出。
    • 然后,x = hidden.squeeze()提取了最后一个时间步的隐藏状态作为后续分类的主要依据 。这是因为最后一个隐藏状态包含了整个字符串的综合信息,模型认为这个信息对于判断特定字符在字符串中的位置是最关键的。最后,这个隐藏状态通过一个线性分类层self.classifier来预测特定字符在字符串中的位置类别。

5、为什么两个模型线性层的输出不一样?

  1. 任务性质决定输出维度
    • 模型 1
      • 任务是判断特定字符在字符串中的位置,这是一个多分类任务。例如,对于长度为 10 的字符串,特定字符可能出现在 0 - 10(包括字符不存在的情况)这 11 个位置中的任意一个。所以线性层的输出需要有与位置类别数量相同的维度,用于表示每个位置的预测概率。因此,模型 1 的线性层输出维度是num_classes(等于字符串长度 + 1)。
    • 模型 2
      • 任务是判断文本中是否有某些特定字符出现,这是一个二分类任务。输出只需要表示文本包含特定字符(正例)或不包含特定字符(反例)这两种情况。所以线性层输出维度为 1,通过激活函数(如sigmoid)将这个输出转换为概率值,表示文本属于正例的概率。
  2. 模型架构和后续处理需求影响输出维度
    • 模型 1
      • 线性层之后通常直接连接用于多分类的损失函数 (如CrossEntropyLoss)。CrossEntropyLoss要求输入的预测概率分布维度与类别数量一致,这样才能正确计算预测概率与真实标签之间的损失。因此,线性层的输出维度需要适配类别数量,以符合损失函数的要求并实现有效的多分类。
    • 模型 2
      • 线性层输出后连接了sigmoid激活函数,将输出转换为概率值用于二分类判断。由于只需要一个数值来表示概率,所以线性层输出维度为 1。 并且,在计算损失时,使用的损失函数(如MSE或更合适的BCELoss)也可以处理这种单维度的预测概率与真实标签(0 或 1)之间的损失计算。

6、为什么模型1的线性层可以不写激活函数?

  1. 任务类型与激活函数的关系
    • 模型 1 是一个多分类任务,用于判断特定字符在字符串中的位置。其线性层输出的目的是产生一个概率分布,用于表示字符出现在各个位置的可能性。
    • 在使用CrossEntropyLoss作为损失函数时,它期望线性层的输出直接是未经过激活函数处理的 "原始分数"(logits)。这些 logits 会在损失函数内部通过softmax函数进行转换,以得到概率分布并计算损失。
  2. CrossEntropyLoss的工作原理与要求
    • CrossEntropyLoss的计算公式涉及到softmax操作 。具体来说,对于线性层输出的 logits 向量z(假设维度为CC是类别数),softmax函数计算概率分布p的公式为
    • 损失函数会根据这个自动计算出的概率分布p和真实标签y来计算交叉熵损失。如果在线性层后添加激活函数(如sigmoid或softmax),会改变线性层输出的数值范围和分布,导致与CrossEntropyLoss内部的softmax操作冲突,或者使模型难以训练。
  3. 线性层输出的合适表示形式
    • 不使用激活函数时,线性层输出的 logits 可以是任意实数范围的值。这种原始的输出形式能够更好地被CrossEntropyLoss利用,通过其内部的softmax操作来正确地计算概率分布,并根据概率分布和真实标签计算损失,从而引导模型学习到正确的类别概率分布,以准确判断特定字符的位置。

7、池化层输出后进行x.squeeze()和rnn输出后hidden.squeeze()是同一个作用吗?

  1. rnn输出后hidden.squeeze()的作用

    • 在模型 1 中,rnn的输出包括整个序列的隐藏状态和最后一个时间步的隐藏状态。当执行rnn_out, hidden = self.rnn(x)后,hidden是最后一个时间步的隐藏状态,其形状可能是(batch_size, 1, hidden_size)(这取决于batch_firstRNN的实现细节)。
    • hidden.squeeze()操作的目的是移除维度为 1 的维度,将形状变为(batch_size, hidden_size)。这样做是为了方便后续将这个隐藏状态输入到线性分类层中。线性分类层通常期望输入的形状是(batch_size, input_size),这里input_size(即hidden_size)是合适的输入维度,通过squeeze操作可以使数据形状符合线性层的输入要求。
  2. 池化层输出后x.squeeze()的作用(以模型 2 为例)

    • 在模型 2 中,经过池化层操作后,例如nn.AvgPool1d,输出的形状可能是(batch_size, vector_dim, 1)。这个额外的维度为 1 的维度可能是因为池化操作在某个维度上进行了压缩(在这个例子中是句子长度维度被压缩为 1)。
    • x.squeeze()操作同样是移除这个维度为 1 的维度,将形状变为(batch_size, vector_dim)。这是为了让输出形状符合后续线性分类层的输入要求,使得数据能够顺利地通过线性层进行进一步的处理,以完成二分类任务。
  3. 总结相同点和不同点

    • 相同点
      • 两个squeeze操作在功能上都是为了调整张量的形状,移除维度为 1 的维度,使得张量的形状更符合后续操作(主要是线性分类层)的输入要求,从而能够顺利地进行后续的神经网络计算。
    • 不同点
      • 它们所处理的数据来源不同。一个是RNN最后一个时间步的隐藏状态,另一个是池化层的输出。而且这些数据在原始形状上的差异是由于不同的神经网络操作(RNN处理序列和池化层压缩维度)导致的,只是在形状调整的目的上(为了适配线性层输入)是相同的。
相关推荐
肉包之2 分钟前
pythonOpenCV篇:0基础带你python入门之常用函数
人工智能·python·opencv·计算机视觉·目标跟踪
康谋自动驾驶9 分钟前
康谋方案 | 多源相机数据采集与算法集成测试方案
人工智能·科技·数据分析·自动驾驶·汽车
小嗷犬18 分钟前
【论文笔记】VisionZip: Longer is Better but Not Necessary in Vision Language Models
论文阅读·人工智能·语言模型·大模型·多模态
gikod19 分钟前
【笔记】架构上篇Day6 法则四:为什么要顺应技术的生命周期?
大数据·人工智能·笔记·架构
Easy数模22 分钟前
经典NLP案例 | 推文评论情绪分析:从数据预处理到模型构建的全面指南
人工智能·自然语言处理·nlp
胡说八道的Dr. Zhu37 分钟前
【图像去雾数据集】URHI数据集介绍
人工智能·计算机视觉
Code成立39 分钟前
《Java核心技术I》并行数组算法
开发语言·python
坐吃山猪43 分钟前
机器学习02-发展历史补充
人工智能·机器学习
网易易盾1 小时前
风控大讲堂|游戏黑产情报挖掘与治理
人工智能·安全
我感觉。1 小时前
【机器学习chp9】集成学习
人工智能·机器学习·集成学习·xgboost·lightgbm·catboost